atomadic-forge 0.3.2__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.
- atomadic_forge/__init__.py +12 -0
- atomadic_forge/__main__.py +5 -0
- atomadic_forge/a0_qk_constants/__init__.py +1 -0
- atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
- atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
- atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
- atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
- atomadic_forge/a0_qk_constants/error_codes.py +296 -0
- atomadic_forge/a0_qk_constants/forge_types.py +89 -0
- atomadic_forge/a0_qk_constants/gen_language.py +116 -0
- atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
- atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
- atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
- atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
- atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
- atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
- atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
- atomadic_forge/a0_qk_constants/tier_names.py +47 -0
- atomadic_forge/a1_at_functions/__init__.py +1 -0
- atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
- atomadic_forge/a1_at_functions/agent_memory.py +139 -0
- atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
- atomadic_forge/a1_at_functions/agent_summary.py +277 -0
- atomadic_forge/a1_at_functions/body_extractor.py +306 -0
- atomadic_forge/a1_at_functions/card_renderer.py +210 -0
- atomadic_forge/a1_at_functions/certify_checks.py +445 -0
- atomadic_forge/a1_at_functions/chat_context.py +170 -0
- atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
- atomadic_forge/a1_at_functions/classify_tier.py +115 -0
- atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
- atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
- atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
- atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
- atomadic_forge/a1_at_functions/config_io.py +68 -0
- atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
- atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
- atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
- atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
- atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
- atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
- atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
- atomadic_forge/a1_at_functions/error_hints.py +105 -0
- atomadic_forge/a1_at_functions/evolution_log.py +94 -0
- atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
- atomadic_forge/a1_at_functions/generation_quality.py +322 -0
- atomadic_forge/a1_at_functions/import_repair.py +211 -0
- atomadic_forge/a1_at_functions/import_smoke.py +102 -0
- atomadic_forge/a1_at_functions/js_parser.py +539 -0
- atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
- atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
- atomadic_forge/a1_at_functions/llm_client.py +554 -0
- atomadic_forge/a1_at_functions/local_signer.py +134 -0
- atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
- atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
- atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
- atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
- atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
- atomadic_forge/a1_at_functions/policy_loader.py +107 -0
- atomadic_forge/a1_at_functions/preflight_change.py +227 -0
- atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
- atomadic_forge/a1_at_functions/provider_detect.py +157 -0
- atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
- atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
- atomadic_forge/a1_at_functions/recipes.py +186 -0
- atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
- atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
- atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
- atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
- atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
- atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
- atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
- atomadic_forge/a1_at_functions/scout_walk.py +309 -0
- atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
- atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
- atomadic_forge/a1_at_functions/stub_detector.py +158 -0
- atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
- atomadic_forge/a1_at_functions/synergy_render.py +252 -0
- atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
- atomadic_forge/a1_at_functions/test_runner.py +196 -0
- atomadic_forge/a1_at_functions/test_selector.py +122 -0
- atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
- atomadic_forge/a1_at_functions/tool_composer.py +130 -0
- atomadic_forge/a1_at_functions/transcript_log.py +70 -0
- atomadic_forge/a1_at_functions/wire_check.py +260 -0
- atomadic_forge/a2_mo_composites/__init__.py +1 -0
- atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
- atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
- atomadic_forge/a2_mo_composites/plan_store.py +164 -0
- atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
- atomadic_forge/a3_og_features/__init__.py +1 -0
- atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
- atomadic_forge/a3_og_features/demo_runner.py +502 -0
- atomadic_forge/a3_og_features/emergent_feature.py +95 -0
- atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
- atomadic_forge/a3_og_features/forge_enforce.py +107 -0
- atomadic_forge/a3_og_features/forge_evolve.py +176 -0
- atomadic_forge/a3_og_features/forge_loop.py +528 -0
- atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
- atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
- atomadic_forge/a3_og_features/lsp_server.py +98 -0
- atomadic_forge/a3_og_features/mcp_server.py +160 -0
- atomadic_forge/a3_og_features/setup_wizard.py +337 -0
- atomadic_forge/a3_og_features/synergy_feature.py +65 -0
- atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
- atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
- atomadic_forge/commands/__init__.py +1 -0
- atomadic_forge/commands/_registry.py +36 -0
- atomadic_forge/commands/audit.py +142 -0
- atomadic_forge/commands/chat.py +133 -0
- atomadic_forge/commands/commandsmith.py +178 -0
- atomadic_forge/commands/config_cmd.py +145 -0
- atomadic_forge/commands/demo.py +142 -0
- atomadic_forge/commands/emergent.py +124 -0
- atomadic_forge/commands/emergent_then_synergy.py +70 -0
- atomadic_forge/commands/evolve.py +122 -0
- atomadic_forge/commands/evolve_then_iterate.py +70 -0
- atomadic_forge/commands/feature_then_emergent.py +111 -0
- atomadic_forge/commands/iterate.py +140 -0
- atomadic_forge/commands/synergy.py +96 -0
- atomadic_forge/commands/synergy_then_emergent.py +70 -0
- atomadic_forge-0.3.2.dist-info/METADATA +471 -0
- atomadic_forge-0.3.2.dist-info/RECORD +131 -0
- atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
- atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
- atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
- atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tier a2 — append-only persistence for agent_plan/v1 documents.
|
|
2
|
+
|
|
3
|
+
Codex's follow-up: 'forge auto step <card-id>' / 'forge auto apply
|
|
4
|
+
<plan-id>' need plans to be addressable. This store gives every plan
|
|
5
|
+
a stable id (SHA-256 prefix of the plan's structural content) and
|
|
6
|
+
persists it under ``.atomadic-forge/plans/<id>.json``. Per-card
|
|
7
|
+
state (applied / rejected / skipped) lives in a sibling
|
|
8
|
+
``.atomadic-forge/plans/<id>.state.json`` so the plan doc itself
|
|
9
|
+
stays append-only and re-emit-safe.
|
|
10
|
+
|
|
11
|
+
Pure-ish: file I/O scoped under one project_root; no network. The
|
|
12
|
+
pure id-derivation lives in a1 (``a1.lineage_chain.canonical_receipt_hash``-style)
|
|
13
|
+
but for plans the canonical fields are different — declared inline
|
|
14
|
+
here to keep the dependency surface tight.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import datetime as _dt
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_DIRNAME = ".atomadic-forge"
|
|
25
|
+
_PLANS_SUBDIR = "plans"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Fields that participate in the plan id hash. Mutable / volatile
|
|
29
|
+
# fields are excluded so re-emitting the same plan yields the same id.
|
|
30
|
+
_PLAN_HASH_INCLUDE: tuple[str, ...] = (
|
|
31
|
+
"schema_version",
|
|
32
|
+
"goal",
|
|
33
|
+
"mode",
|
|
34
|
+
"project_root",
|
|
35
|
+
"top_actions",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compute_plan_id(plan: dict) -> str:
|
|
40
|
+
"""SHA-256 hex prefix derived from the plan's structural content."""
|
|
41
|
+
canonical = {k: plan.get(k) for k in _PLAN_HASH_INCLUDE}
|
|
42
|
+
blob = json.dumps(canonical, sort_keys=True, default=str)
|
|
43
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:12]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _now_utc_iso() -> str:
|
|
47
|
+
return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PlanStore:
|
|
51
|
+
"""Append-only plan + per-card-state persistence."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, project_root: Path) -> None:
|
|
54
|
+
self.project_root = Path(project_root).resolve()
|
|
55
|
+
self.dir = self.project_root / _DIRNAME / _PLANS_SUBDIR
|
|
56
|
+
|
|
57
|
+
# ---- write paths ---------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def save_plan(self, plan: dict) -> str:
|
|
60
|
+
"""Persist a plan dict and return its id (idempotent)."""
|
|
61
|
+
plan_id = plan.get("id") or compute_plan_id(plan)
|
|
62
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
# Persist the plan with its id baked in so repeat saves are safe.
|
|
64
|
+
out = dict(plan)
|
|
65
|
+
out.setdefault("id", plan_id)
|
|
66
|
+
out.setdefault("saved_at_utc", _now_utc_iso())
|
|
67
|
+
target = self.dir / f"{plan_id}.json"
|
|
68
|
+
target.write_text(
|
|
69
|
+
json.dumps(out, indent=2, default=str), encoding="utf-8")
|
|
70
|
+
# Initialize state file if missing.
|
|
71
|
+
state_path = self._state_path(plan_id)
|
|
72
|
+
if not state_path.exists():
|
|
73
|
+
state_path.write_text(
|
|
74
|
+
json.dumps({
|
|
75
|
+
"schema_version": "atomadic-forge.plan_state/v1",
|
|
76
|
+
"plan_id": plan_id,
|
|
77
|
+
"events": [],
|
|
78
|
+
}, indent=2),
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
)
|
|
81
|
+
return plan_id
|
|
82
|
+
|
|
83
|
+
def record_card_event(
|
|
84
|
+
self,
|
|
85
|
+
plan_id: str,
|
|
86
|
+
*,
|
|
87
|
+
card_id: str,
|
|
88
|
+
status: str,
|
|
89
|
+
detail: dict[str, Any] | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Append a per-card outcome (applied / rejected / skipped /
|
|
92
|
+
rolled_back). Pure append; never rewrites prior events."""
|
|
93
|
+
state = self.load_state(plan_id) or {
|
|
94
|
+
"schema_version": "atomadic-forge.plan_state/v1",
|
|
95
|
+
"plan_id": plan_id, "events": [],
|
|
96
|
+
}
|
|
97
|
+
state["events"].append({
|
|
98
|
+
"ts_utc": _now_utc_iso(),
|
|
99
|
+
"card_id": card_id,
|
|
100
|
+
"status": status,
|
|
101
|
+
"detail": detail or {},
|
|
102
|
+
})
|
|
103
|
+
self._state_path(plan_id).write_text(
|
|
104
|
+
json.dumps(state, indent=2, default=str), encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
# ---- read paths ----------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def load_plan(self, plan_id: str) -> dict | None:
|
|
109
|
+
target = self.dir / f"{plan_id}.json"
|
|
110
|
+
if not target.exists():
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
114
|
+
except (OSError, json.JSONDecodeError):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def load_state(self, plan_id: str) -> dict | None:
|
|
118
|
+
target = self._state_path(plan_id)
|
|
119
|
+
if not target.exists():
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
123
|
+
except (OSError, json.JSONDecodeError):
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def list_plans(self) -> list[dict]:
|
|
127
|
+
"""Return summaries newest-first by saved_at_utc."""
|
|
128
|
+
if not self.dir.exists():
|
|
129
|
+
return []
|
|
130
|
+
out: list[dict] = []
|
|
131
|
+
for f in self.dir.glob("*.json"):
|
|
132
|
+
if f.name.endswith(".state.json"):
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
doc = json.loads(f.read_text(encoding="utf-8"))
|
|
136
|
+
except (OSError, json.JSONDecodeError):
|
|
137
|
+
continue
|
|
138
|
+
out.append({
|
|
139
|
+
"plan_id": doc.get("id", f.stem),
|
|
140
|
+
"verdict": doc.get("verdict", "?"),
|
|
141
|
+
"goal": doc.get("goal", ""),
|
|
142
|
+
"mode": doc.get("mode", ""),
|
|
143
|
+
"action_count": doc.get("action_count", 0),
|
|
144
|
+
"applyable_count": doc.get("applyable_count", 0),
|
|
145
|
+
"saved_at_utc": doc.get("saved_at_utc", ""),
|
|
146
|
+
})
|
|
147
|
+
out.sort(key=lambda d: d["saved_at_utc"], reverse=True)
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
def card_status(self, plan_id: str, card_id: str) -> str:
|
|
151
|
+
"""Return the latest status for ``card_id`` in plan, or
|
|
152
|
+
'unapplied' when no event has been recorded."""
|
|
153
|
+
state = self.load_state(plan_id)
|
|
154
|
+
if not state:
|
|
155
|
+
return "unapplied"
|
|
156
|
+
for ev in reversed(state.get("events", [])):
|
|
157
|
+
if ev.get("card_id") == card_id:
|
|
158
|
+
return ev.get("status", "unapplied")
|
|
159
|
+
return "unapplied"
|
|
160
|
+
|
|
161
|
+
# ---- helpers -------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def _state_path(self, plan_id: str) -> Path:
|
|
164
|
+
return self.dir / f"{plan_id}.state.json"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Tier a2 — Receipt signing client.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W2. Calls AAAA-Nexus ``/v1/verify/forge-receipt`` to
|
|
4
|
+
obtain Sigstore Rekor metadata + AAAA-Nexus signature, mutates the
|
|
5
|
+
Receipt in place, and returns it.
|
|
6
|
+
|
|
7
|
+
Stateful by design: holds base URL + auth env name + last-known
|
|
8
|
+
endpoint health (so a single CLI session probing once doesn't probe
|
|
9
|
+
again on every Receipt). Fits a2 cleanly (composes a1 emitter output
|
|
10
|
+
+ a0 schema constants + std-lib I/O); never imports a3+.
|
|
11
|
+
|
|
12
|
+
Graceful-degradation contract:
|
|
13
|
+
* If ``AAAA_NEXUS_API_KEY`` is unset → return the Receipt unchanged
|
|
14
|
+
with a ``notes`` entry explaining why. No exception.
|
|
15
|
+
* If the endpoint returns 4xx (trial-tier rate limit, key invalid,
|
|
16
|
+
feature not yet shipped) → same: return unsigned with a note.
|
|
17
|
+
* Network / DNS / timeout error → same: return unsigned with a note
|
|
18
|
+
that includes the underlying error class.
|
|
19
|
+
* 5xx → caller's choice: ``strict=True`` raises ``RuntimeError``;
|
|
20
|
+
``strict=False`` (default) returns unsigned + note.
|
|
21
|
+
|
|
22
|
+
Why the soft-fail default: per the Golden Path, "unsigned Receipts
|
|
23
|
+
are valid. The signature fields default to None." A local-development
|
|
24
|
+
``forge certify --sign`` invocation should never crash because the
|
|
25
|
+
prod endpoint is having a bad day. CI gates that REQUIRE a signature
|
|
26
|
+
should pass ``--sign --require-signed`` (Lane G future work).
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.request
|
|
34
|
+
from copy import deepcopy
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from ..a0_qk_constants.receipt_schema import (
|
|
38
|
+
ForgeReceiptV1,
|
|
39
|
+
ReceiptAAAANexusSignature,
|
|
40
|
+
ReceiptSigstoreSignature,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReceiptSigner:
|
|
45
|
+
"""Stateful signer that wraps the AAAA-Nexus verify endpoint."""
|
|
46
|
+
|
|
47
|
+
DEFAULT_BASE_URL = "https://aaaa-nexus.atomadictech.workers.dev"
|
|
48
|
+
DEFAULT_PATH = "/v1/verify/forge-receipt"
|
|
49
|
+
DEFAULT_TIMEOUT_SECONDS = 30
|
|
50
|
+
USER_AGENT = "atomadic-forge/0.2 (ReceiptSigner)"
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
base_url: str | None = None,
|
|
56
|
+
api_key_env: str = "AAAA_NEXUS_API_KEY",
|
|
57
|
+
path: str | None = None,
|
|
58
|
+
timeout_seconds: int | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.base_url = (
|
|
61
|
+
base_url
|
|
62
|
+
or os.environ.get("AAAA_NEXUS_URL")
|
|
63
|
+
or self.DEFAULT_BASE_URL
|
|
64
|
+
).rstrip("/")
|
|
65
|
+
self.api_key_env = api_key_env
|
|
66
|
+
self.path = path or self.DEFAULT_PATH
|
|
67
|
+
self.timeout_seconds = (
|
|
68
|
+
timeout_seconds
|
|
69
|
+
if timeout_seconds is not None
|
|
70
|
+
else self.DEFAULT_TIMEOUT_SECONDS
|
|
71
|
+
)
|
|
72
|
+
# Soft cache of last-known reachability so a session that
|
|
73
|
+
# probed once and got 404 doesn't re-probe on every receipt.
|
|
74
|
+
self._endpoint_known_unavailable = False
|
|
75
|
+
|
|
76
|
+
# ---- public surface ------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def sign(
|
|
79
|
+
self,
|
|
80
|
+
receipt: ForgeReceiptV1,
|
|
81
|
+
*,
|
|
82
|
+
strict: bool = False,
|
|
83
|
+
) -> ForgeReceiptV1:
|
|
84
|
+
"""Return a new Receipt with the ``signatures`` block populated.
|
|
85
|
+
|
|
86
|
+
The input Receipt is not mutated. The output is a deep copy
|
|
87
|
+
with ``signatures.sigstore`` and ``signatures.aaaa_nexus``
|
|
88
|
+
either populated (success) or left at None with a ``notes``
|
|
89
|
+
entry (soft-fail).
|
|
90
|
+
"""
|
|
91
|
+
out = deepcopy(receipt)
|
|
92
|
+
api_key = os.environ.get(self.api_key_env, "")
|
|
93
|
+
if not api_key:
|
|
94
|
+
self._note(out, f"{self.api_key_env} not set — receipt left unsigned")
|
|
95
|
+
return out
|
|
96
|
+
if self._endpoint_known_unavailable:
|
|
97
|
+
self._note(out, "AAAA-Nexus signing endpoint unavailable this session")
|
|
98
|
+
return out
|
|
99
|
+
try:
|
|
100
|
+
response = self._post_for_signature(receipt, api_key)
|
|
101
|
+
except (urllib.error.HTTPError, urllib.error.URLError, OSError,
|
|
102
|
+
ValueError) as exc:
|
|
103
|
+
return self._handle_request_failure(out, exc, strict=strict)
|
|
104
|
+
self._apply_signature(out, response)
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
# ---- request helpers ----------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _post_for_signature(
|
|
110
|
+
self,
|
|
111
|
+
receipt: ForgeReceiptV1,
|
|
112
|
+
api_key: str,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
body = json.dumps({"receipt": receipt}, default=str).encode("utf-8")
|
|
115
|
+
endpoint = self.base_url + self.path
|
|
116
|
+
req = urllib.request.Request(
|
|
117
|
+
endpoint,
|
|
118
|
+
data=body,
|
|
119
|
+
headers={
|
|
120
|
+
"X-API-Key": api_key,
|
|
121
|
+
"Authorization": f"Bearer {api_key}",
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"User-Agent": self.USER_AGENT,
|
|
124
|
+
"Accept": "application/json",
|
|
125
|
+
},
|
|
126
|
+
method="POST",
|
|
127
|
+
)
|
|
128
|
+
with urllib.request.urlopen(req, timeout=self.timeout_seconds) as resp:
|
|
129
|
+
payload = resp.read()
|
|
130
|
+
decoded = json.loads(payload.decode("utf-8") or "{}")
|
|
131
|
+
if not isinstance(decoded, dict):
|
|
132
|
+
raise ValueError("AAAA-Nexus returned non-object body")
|
|
133
|
+
return decoded
|
|
134
|
+
|
|
135
|
+
def _apply_signature(
|
|
136
|
+
self,
|
|
137
|
+
receipt: ForgeReceiptV1,
|
|
138
|
+
response: dict[str, Any],
|
|
139
|
+
) -> None:
|
|
140
|
+
sigstore = response.get("sigstore")
|
|
141
|
+
nexus = response.get("aaaa_nexus") or response.get("aaaa-nexus")
|
|
142
|
+
sigs = receipt.get("signatures") or {}
|
|
143
|
+
if isinstance(sigstore, dict):
|
|
144
|
+
sigs["sigstore"] = ReceiptSigstoreSignature( # type: ignore[typeddict-item]
|
|
145
|
+
rekor_uuid=str(sigstore.get("rekor_uuid", "")),
|
|
146
|
+
log_index=int(sigstore.get("log_index", 0)),
|
|
147
|
+
bundle_path=str(sigstore.get("bundle_path", "")),
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
sigs["sigstore"] = None # type: ignore[typeddict-item]
|
|
151
|
+
if isinstance(nexus, dict):
|
|
152
|
+
sigs["aaaa_nexus"] = ReceiptAAAANexusSignature( # type: ignore[typeddict-item]
|
|
153
|
+
signature=str(nexus.get("signature", "")),
|
|
154
|
+
key_id=str(nexus.get("key_id", "")),
|
|
155
|
+
issuer=str(nexus.get("issuer", "")),
|
|
156
|
+
issued_at_utc=str(nexus.get("issued_at_utc", "")),
|
|
157
|
+
verify_endpoint=str(nexus.get("verify_endpoint", self.path)),
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
sigs["aaaa_nexus"] = None # type: ignore[typeddict-item]
|
|
161
|
+
receipt["signatures"] = sigs # type: ignore[typeddict-item]
|
|
162
|
+
|
|
163
|
+
# ---- failure handling ---------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _handle_request_failure(
|
|
166
|
+
self,
|
|
167
|
+
receipt: ForgeReceiptV1,
|
|
168
|
+
exc: BaseException,
|
|
169
|
+
*,
|
|
170
|
+
strict: bool,
|
|
171
|
+
) -> ForgeReceiptV1:
|
|
172
|
+
# 4xx: known bad request / auth / not-yet-shipped — never retry,
|
|
173
|
+
# mark endpoint unavailable for the session so subsequent calls
|
|
174
|
+
# in this run skip the round trip.
|
|
175
|
+
if isinstance(exc, urllib.error.HTTPError):
|
|
176
|
+
if 400 <= exc.code < 500:
|
|
177
|
+
self._endpoint_known_unavailable = True
|
|
178
|
+
self._note(
|
|
179
|
+
receipt,
|
|
180
|
+
f"AAAA-Nexus signing returned HTTP {exc.code}; "
|
|
181
|
+
"receipt left unsigned",
|
|
182
|
+
)
|
|
183
|
+
return receipt
|
|
184
|
+
if strict:
|
|
185
|
+
raise RuntimeError(
|
|
186
|
+
f"AAAA-Nexus signing failed with HTTP {exc.code}: {exc}"
|
|
187
|
+
) from exc
|
|
188
|
+
self._note(
|
|
189
|
+
receipt,
|
|
190
|
+
f"AAAA-Nexus signing failed with HTTP {exc.code}; "
|
|
191
|
+
"soft-fail, receipt left unsigned",
|
|
192
|
+
)
|
|
193
|
+
return receipt
|
|
194
|
+
if strict:
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f"AAAA-Nexus signing transport error: "
|
|
197
|
+
f"{type(exc).__name__}: {exc}"
|
|
198
|
+
) from exc
|
|
199
|
+
self._note(
|
|
200
|
+
receipt,
|
|
201
|
+
f"AAAA-Nexus signing transport error "
|
|
202
|
+
f"({type(exc).__name__}); receipt left unsigned",
|
|
203
|
+
)
|
|
204
|
+
return receipt
|
|
205
|
+
|
|
206
|
+
# ---- helpers -------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _note(receipt: ForgeReceiptV1, message: str) -> None:
|
|
210
|
+
notes = list(receipt.get("notes") or [])
|
|
211
|
+
notes.append(message)
|
|
212
|
+
receipt["notes"] = notes # type: ignore[typeddict-item]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def sign_receipt(
|
|
216
|
+
receipt: ForgeReceiptV1,
|
|
217
|
+
*,
|
|
218
|
+
base_url: str | None = None,
|
|
219
|
+
api_key_env: str = "AAAA_NEXUS_API_KEY",
|
|
220
|
+
strict: bool = False,
|
|
221
|
+
) -> ForgeReceiptV1:
|
|
222
|
+
"""Module-level convenience: instantiate a signer and call ``sign``.
|
|
223
|
+
|
|
224
|
+
For one-shot CLI calls. Long-running processes should construct a
|
|
225
|
+
``ReceiptSigner`` once and reuse it (the endpoint-availability
|
|
226
|
+
cache is per-instance).
|
|
227
|
+
"""
|
|
228
|
+
return ReceiptSigner(
|
|
229
|
+
base_url=base_url,
|
|
230
|
+
api_key_env=api_key_env,
|
|
231
|
+
).sign(receipt, strict=strict)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tier a3 — features compose a2 composites + a1 helpers into capabilities."""
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tier a3 — Commandsmith feature: discover, register, document, smoke-test.
|
|
2
|
+
|
|
3
|
+
The Commandsmith feature glues together the a1 discoverer + a1 renderers and
|
|
4
|
+
manages on-disk persistence for the registry. It is the only module the a4
|
|
5
|
+
CLI surface depends on.
|
|
6
|
+
|
|
7
|
+
Pipelines exposed:
|
|
8
|
+
|
|
9
|
+
* :meth:`sync` — full pipeline (discover → render registry → write docs).
|
|
10
|
+
* :meth:`wrap_class` — generate a Typer wrapper around a single class
|
|
11
|
+
(used to lift a freshly assimilated symbol into a CLI command).
|
|
12
|
+
* :meth:`smoke` — invoke ``--help`` on every registered command and record
|
|
13
|
+
exit codes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import datetime as _dt
|
|
19
|
+
import importlib
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from ..a0_qk_constants.commandsmith_types import (
|
|
26
|
+
CommandSignatureCard,
|
|
27
|
+
RegisteredCommandCard,
|
|
28
|
+
RegistryManifestCard,
|
|
29
|
+
)
|
|
30
|
+
from ..a1_at_functions.commandsmith_discover import discover_command_modules
|
|
31
|
+
from ..a1_at_functions.commandsmith_render import (
|
|
32
|
+
render_command_doc,
|
|
33
|
+
render_command_index,
|
|
34
|
+
render_registry_module,
|
|
35
|
+
render_wrapper_module,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_REGISTRY_FILE = "_registry.py"
|
|
39
|
+
_MANIFEST_FILE = "commandsmith_manifest.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Commandsmith:
|
|
43
|
+
"""Manage the CLI registry for an ASS-ADE-style package.
|
|
44
|
+
|
|
45
|
+
``package_root``: directory containing the ``commands/`` subfolder.
|
|
46
|
+
``docs_root``: where to write per-command Markdown.
|
|
47
|
+
``manifest_dir``: where to persist ``commandsmith_manifest.json``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
package_root: Path,
|
|
54
|
+
package_name: str = "atomadic_forge",
|
|
55
|
+
docs_root: Path | None = None,
|
|
56
|
+
manifest_dir: Path | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.package_root = Path(package_root)
|
|
59
|
+
self.package_name = package_name
|
|
60
|
+
self.commands_dir = self.package_root / "commands"
|
|
61
|
+
self.docs_root = Path(docs_root) if docs_root else self.package_root.parent.parent / "docs" / "commands"
|
|
62
|
+
self.manifest_dir = Path(manifest_dir) if manifest_dir else self.package_root.parent.parent / ".atomadic-forge"
|
|
63
|
+
|
|
64
|
+
# ----- discovery --------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def discover(self) -> list[RegisteredCommandCard]:
|
|
67
|
+
return discover_command_modules(
|
|
68
|
+
self.package_root,
|
|
69
|
+
package=self.package_name,
|
|
70
|
+
sub_dirs=("commands",),
|
|
71
|
+
source_root="atomadic_forge_seed",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ----- registry / docs / manifest --------------------------------
|
|
75
|
+
|
|
76
|
+
def write_registry(self, cards: list[RegisteredCommandCard]) -> Path:
|
|
77
|
+
"""Emit ``commands/_registry.py``."""
|
|
78
|
+
target = self.commands_dir / _REGISTRY_FILE
|
|
79
|
+
target.write_text(render_registry_module(cards), encoding="utf-8")
|
|
80
|
+
return target
|
|
81
|
+
|
|
82
|
+
def write_docs(self, cards: list[RegisteredCommandCard]) -> list[Path]:
|
|
83
|
+
"""Write per-command Markdown plus an INDEX."""
|
|
84
|
+
self.docs_root.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
out: list[Path] = []
|
|
86
|
+
for card in cards:
|
|
87
|
+
f = self.docs_root / f"{card['name']}.md"
|
|
88
|
+
f.write_text(render_command_doc(card), encoding="utf-8")
|
|
89
|
+
out.append(f)
|
|
90
|
+
idx = self.docs_root / "INDEX.md"
|
|
91
|
+
idx.write_text(render_command_index(cards), encoding="utf-8")
|
|
92
|
+
out.append(idx)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
def write_manifest(self, cards: list[RegisteredCommandCard],
|
|
96
|
+
smoke_results: dict[str, bool] | None = None) -> Path:
|
|
97
|
+
manifest = RegistryManifestCard(
|
|
98
|
+
schema_version="atomadic-forge.commandsmith.registry/v1",
|
|
99
|
+
generated_at_utc=_dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
100
|
+
commands=cards,
|
|
101
|
+
smoke_results=smoke_results or {},
|
|
102
|
+
)
|
|
103
|
+
self.manifest_dir.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
target = self.manifest_dir / _MANIFEST_FILE
|
|
105
|
+
target.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
106
|
+
return target
|
|
107
|
+
|
|
108
|
+
# ----- generator wrapping a class --------------------------------
|
|
109
|
+
|
|
110
|
+
def wrap_class(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
target_module: str,
|
|
114
|
+
target_class: str,
|
|
115
|
+
command_name: str,
|
|
116
|
+
out_dir: Path | None = None,
|
|
117
|
+
help_text: str = "",
|
|
118
|
+
auto_scan: str | None = None,
|
|
119
|
+
) -> Path:
|
|
120
|
+
"""Generate ``commands/<command_name>_cli.py`` wrapping a class.
|
|
121
|
+
|
|
122
|
+
``auto_scan`` (optional): name of a method to call inside the Typer
|
|
123
|
+
callback (e.g. ``scan_repo`` for CherryPicker) so subcommands see a
|
|
124
|
+
populated instance state. If omitted, a sensible default is picked:
|
|
125
|
+
any method whose name is exactly ``scan`` or starts with ``scan_``.
|
|
126
|
+
"""
|
|
127
|
+
sub_cmds = self._inspect_class_methods(target_module, target_class)
|
|
128
|
+
init_params = self._inspect_init_params(target_module, target_class)
|
|
129
|
+
if auto_scan is None:
|
|
130
|
+
auto_scan = next(
|
|
131
|
+
(s["name"] for s in sub_cmds
|
|
132
|
+
if s["name"] == "scan" or s["name"].startswith("scan_")),
|
|
133
|
+
None,
|
|
134
|
+
)
|
|
135
|
+
src = render_wrapper_module(
|
|
136
|
+
target_module=target_module,
|
|
137
|
+
target_class=target_class,
|
|
138
|
+
command_name=command_name,
|
|
139
|
+
sub_commands=sub_cmds,
|
|
140
|
+
help_text=help_text or f"Auto-wrapped {target_class}",
|
|
141
|
+
init_params=init_params,
|
|
142
|
+
auto_scan=auto_scan,
|
|
143
|
+
)
|
|
144
|
+
out = (out_dir or self.commands_dir) / f"{command_name.replace('-', '_')}_cli.py"
|
|
145
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
out.write_text(src, encoding="utf-8")
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
def _inspect_init_params(self, target_module: str,
|
|
150
|
+
target_class: str) -> list[str]:
|
|
151
|
+
"""Extract __init__ parameter strings (``name: type``)."""
|
|
152
|
+
import inspect
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
module = importlib.import_module(target_module)
|
|
156
|
+
cls = getattr(module, target_class)
|
|
157
|
+
sig = inspect.signature(cls.__init__)
|
|
158
|
+
params: list[str] = []
|
|
159
|
+
for pname, p in sig.parameters.items():
|
|
160
|
+
if pname == "self":
|
|
161
|
+
continue
|
|
162
|
+
if p.kind in (inspect.Parameter.VAR_POSITIONAL,
|
|
163
|
+
inspect.Parameter.VAR_KEYWORD):
|
|
164
|
+
continue
|
|
165
|
+
ann = (
|
|
166
|
+
p.annotation.__name__
|
|
167
|
+
if p.annotation is not inspect.Parameter.empty
|
|
168
|
+
and hasattr(p.annotation, "__name__")
|
|
169
|
+
else str(p.annotation)
|
|
170
|
+
if p.annotation is not inspect.Parameter.empty
|
|
171
|
+
else "str"
|
|
172
|
+
)
|
|
173
|
+
params.append(f"{pname}: {ann}")
|
|
174
|
+
return params
|
|
175
|
+
except Exception:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
def _inspect_class_methods(
|
|
179
|
+
self, target_module: str, target_class: str,
|
|
180
|
+
) -> list[CommandSignatureCard]:
|
|
181
|
+
"""Live-inspect a class via importlib and return method signatures.
|
|
182
|
+
|
|
183
|
+
Falls back to AST scanning if import fails.
|
|
184
|
+
"""
|
|
185
|
+
import inspect
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
module = importlib.import_module(target_module)
|
|
189
|
+
cls = getattr(module, target_class)
|
|
190
|
+
cards: list[CommandSignatureCard] = []
|
|
191
|
+
for name, member in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
192
|
+
if name.startswith("_"):
|
|
193
|
+
continue
|
|
194
|
+
sig = inspect.signature(member)
|
|
195
|
+
params: list[str] = []
|
|
196
|
+
for pname, p in sig.parameters.items():
|
|
197
|
+
if pname == "self":
|
|
198
|
+
continue
|
|
199
|
+
ann = (
|
|
200
|
+
p.annotation.__name__
|
|
201
|
+
if p.annotation is not inspect.Parameter.empty
|
|
202
|
+
and hasattr(p.annotation, "__name__")
|
|
203
|
+
else str(p.annotation)
|
|
204
|
+
if p.annotation is not inspect.Parameter.empty
|
|
205
|
+
else "Any"
|
|
206
|
+
)
|
|
207
|
+
params.append(f"{pname}: {ann}")
|
|
208
|
+
ret = (
|
|
209
|
+
sig.return_annotation.__name__
|
|
210
|
+
if sig.return_annotation is not inspect.Signature.empty
|
|
211
|
+
and hasattr(sig.return_annotation, "__name__")
|
|
212
|
+
else "Any"
|
|
213
|
+
)
|
|
214
|
+
cards.append(CommandSignatureCard(
|
|
215
|
+
name=name,
|
|
216
|
+
parameters=params,
|
|
217
|
+
return_type=ret,
|
|
218
|
+
docstring=(inspect.getdoc(member) or "").split("\n")[0],
|
|
219
|
+
))
|
|
220
|
+
return cards
|
|
221
|
+
except Exception:
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
# ----- smoke test -------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def smoke(self, cards: list[RegisteredCommandCard],
|
|
227
|
+
cli_invocation: list[str] | None = None) -> dict[str, bool]:
|
|
228
|
+
"""Run ``<cli> <verb> --help`` for each card; record exit code == 0.
|
|
229
|
+
|
|
230
|
+
Subprocesses inherit ``PYTHONIOENCODING=utf-8`` so rich/Typer help
|
|
231
|
+
rendering doesn't blow up on Windows ``cp1252`` consoles.
|
|
232
|
+
"""
|
|
233
|
+
import os
|
|
234
|
+
if cli_invocation is None:
|
|
235
|
+
# Use the forge CLI entry point.
|
|
236
|
+
cli_invocation = [sys.executable, "-m",
|
|
237
|
+
"atomadic_forge.a4_sy_orchestration.cli"]
|
|
238
|
+
env = os.environ.copy()
|
|
239
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
240
|
+
env["PYTHONUTF8"] = "1"
|
|
241
|
+
results: dict[str, bool] = {}
|
|
242
|
+
for card in cards:
|
|
243
|
+
try:
|
|
244
|
+
rc = subprocess.run(
|
|
245
|
+
cli_invocation + [card["name"], "--help"],
|
|
246
|
+
capture_output=True, text=True, timeout=20,
|
|
247
|
+
env=env, encoding="utf-8", errors="replace",
|
|
248
|
+
).returncode
|
|
249
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
250
|
+
rc = 1
|
|
251
|
+
results[card["name"]] = rc == 0
|
|
252
|
+
return results
|
|
253
|
+
|
|
254
|
+
# ----- one-shot pipeline -----------------------------------------
|
|
255
|
+
|
|
256
|
+
def sync(self, *, smoke: bool = False) -> RegistryManifestCard:
|
|
257
|
+
cards = self.discover()
|
|
258
|
+
self.write_registry(cards)
|
|
259
|
+
self.write_docs(cards)
|
|
260
|
+
smoke_results = self.smoke(cards) if smoke else {}
|
|
261
|
+
self.write_manifest(cards, smoke_results)
|
|
262
|
+
return RegistryManifestCard(
|
|
263
|
+
schema_version="atomadic-forge.commandsmith.registry/v1",
|
|
264
|
+
generated_at_utc=_dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
265
|
+
commands=cards,
|
|
266
|
+
smoke_results=smoke_results,
|
|
267
|
+
)
|
atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Tier a1 — pure helpers for mixed_pkg. May import a0 only."""
|
|
2
|
+
|
|
3
|
+
from mixed_pkg.a0_qk_constants import GREETING_PREFIX, MAX_NAME_LEN
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def greet(name: str) -> str:
|
|
7
|
+
"""Return ``"hello, <name>"`` with a length cap. Pure."""
|
|
8
|
+
safe = name[:MAX_NAME_LEN].strip() or "world"
|
|
9
|
+
return f"{GREETING_PREFIX}, {safe}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def length_within(name: str) -> bool:
|
|
13
|
+
"""Return True if the name is at most MAX_NAME_LEN characters. Pure."""
|
|
14
|
+
return len(name) <= MAX_NAME_LEN
|