-
Notifications
You must be signed in to change notification settings - Fork 218
Expand file tree
/
Copy pathcontext.py
More file actions
177 lines (156 loc) · 9.39 KB
/
context.py
File metadata and controls
177 lines (156 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Mutable state passed between install pipeline phases.
Each phase is a function ``def run(ctx: InstallContext) -> None`` that reads
the inputs already populated by earlier phases and writes its own outputs to
the context. Keeping shared state on a single typed object turns implicit
shared lexical scope (the legacy 1444-line ``_install_apm_dependencies``)
into explicit data flow that is easy to audit and to test phase-by-phase.
Fields are added to this dataclass incrementally as phases are extracted from
the legacy entry point. A field belongs here if and only if it is read or
written by more than one phase. Phase-local state should stay local.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple # noqa: F401, UP035
@dataclass
class InstallContext:
"""State shared across install pipeline phases.
Required-on-construction fields go above the ``field(default=...)``
barrier; outputs accumulated by phases use ``field(default_factory=...)``.
Fields are grouped by the phase that first populates them. A trailing
comment ``# <phase>`` marks the originating phase for auditability.
"""
# ------------------------------------------------------------------
# Required on construction (caller supplies before any phase runs)
# ------------------------------------------------------------------
project_root: Path
apm_dir: Path
# ------------------------------------------------------------------
# Inputs: populated by the caller from CLI args / APMPackage
# ------------------------------------------------------------------
apm_package: Any = None # APMPackage
update_refs: bool = False
scope: Any = None # InstallScope (defaults to PROJECT)
auth_resolver: Any = None # AuthResolver
marketplace_provenance: dict[str, Any] | None = None
parallel_downloads: int = 4
logger: Any = None # InstallLogger
target_override: str | None = None # CLI --target value
allow_insecure: bool = False
allow_insecure_hosts: tuple[str, ...] = ()
dry_run: bool = False
force: bool = False
verbose: bool = False
refresh: bool = False
dev: bool = False
only_packages: list[str] | None = None
protocol_pref: Any = None # ProtocolPreference (NONE/SSH/HTTPS) for shorthand transport
allow_protocol_fallback: bool | None = None # None => read APM_ALLOW_PROTOCOL_FALLBACK env
# ------------------------------------------------------------------
# Resolve phase outputs
# ------------------------------------------------------------------
# Direct dependencies declared in apm.yml (regular + dev), NOT the
# full transitive closure. Transitive deps are discovered later by
# the resolver and recorded on `deps_to_install` /
# `dependency_graph`. Treat `all_apm_deps` as "what the project
# author wrote" -- iterate `deps_to_install` for the full set of
# packages that will be installed.
all_apm_deps: list[Any] = field(default_factory=list) # resolve
root_has_local_primitives: bool = False # resolve
deps_to_install: list[Any] = field(default_factory=list) # resolve
dependency_graph: Any = None # resolve
existing_lockfile: Any = None # resolve
lockfile_path: Path | None = None # resolve
apm_modules_dir: Path | None = None # resolve
downloader: Any = None # resolve (GitHubPackageDownloader)
ref_resolver: Any = None # resolve (TieredRefResolver | None) -- #1369 fast-path
callback_downloaded: dict[str, Any] = field(default_factory=dict) # resolve
callback_failures: set[str] = field(default_factory=set) # resolve
transitive_failures: list[tuple[str, str]] = field(default_factory=list) # resolve
# ------------------------------------------------------------------
# Targets phase outputs
# ------------------------------------------------------------------
targets: list[Any] = field(default_factory=list) # targets
integrators: dict[str, Any] = field(default_factory=dict) # targets
# ------------------------------------------------------------------
# Download phase outputs
# ------------------------------------------------------------------
pre_download_results: dict[str, Any] = field(default_factory=dict) # download
pre_downloaded_keys: set[str] = field(default_factory=set) # download
# ------------------------------------------------------------------
# Pre-integrate inputs (populated by caller before integrate phase)
# ------------------------------------------------------------------
diagnostics: Any = None # DiagnosticCollector
registry_config: Any = None # RegistryConfig (proxy registry; pre-existing)
registry_resolver: Any = None # RegistryPackageResolver -- dedicated registry resolver
# Per-dep git-source semver resolutions (issue #1488). Keyed by
# dep_key (DependencyReference.get_unique_key()), populated by the
# BFS download_callback when a git-source dep has ref_kind == "semver",
# consumed by install/sources.py to plumb the resolution into the
# lockfile via InstalledPackage.git_semver_resolution.
git_semver_resolutions: dict[str, Any] = field(default_factory=dict)
managed_files: set[str] = field(default_factory=set)
# ------------------------------------------------------------------
# Integrate phase outputs (written by integrate, read by cleanup/lockfile/summary)
# ------------------------------------------------------------------
intended_dep_keys: set[str] = field(default_factory=set)
package_deployed_files: dict[str, list[str]] = field(default_factory=dict)
package_types: dict[str, str] = field(default_factory=dict)
package_hashes: dict[str, str] = field(default_factory=dict)
content_hash_verified_deps: set[str] = field(default_factory=set)
# Deps whose content hash is expected to change legitimately:
# populated by _resolve_download_strategy in phases/integrate.py
# (branch-ref `remote_drifted` guard and v<=0.12.2 self-heal block),
# and by the BFS callback in phases/resolve.py (spec-drift detection
# via detect_ref_change). Consumed by
# FreshDependencySource.acquire() in install/sources.py:~624 to
# suppress the supply-chain hard-block when a fresh-download
# content_hash legitimately differs from the lockfile-recorded
# content_hash (drift / recovery, not a supply-chain attack).
expected_hash_change_deps: set[str] = field(default_factory=set)
installed_count: int = 0 # integrate
unpinned_count: int = 0 # integrate
installed_packages: list[Any] = field(default_factory=list) # integrate
total_prompts_integrated: int = 0 # integrate
total_agents_integrated: int = 0 # integrate
total_skills_integrated: int = 0 # integrate
total_sub_skills_promoted: int = 0 # integrate
total_instructions_integrated: int = 0 # integrate
total_commands_integrated: int = 0 # integrate
total_hooks_integrated: int = 0 # integrate
total_links_resolved: int = 0 # integrate
direct_dep_failed: bool = False # integrate -- set when any direct dep fails
# ------------------------------------------------------------------
# policy_gate
# ------------------------------------------------------------------
policy_fetch: Any = None # Optional[PolicyFetchResult] from discovery
policy_enforcement_active: bool = False
no_policy: bool = False # W2-escape-hatch will wire --no-policy here
skill_subset: tuple[str, ...] | None = None # --skill filter for SKILL_BUNDLE packages
skill_subset_from_cli: bool = False # True when user passed --skill (even --skill '*')
early_lockfile: Any = None # LockFile read before pipeline phases (avoids re-read)
direct_mcp_deps: list[Any] | None = None # Direct MCP deps from apm.yml for policy gate
# ------------------------------------------------------------------
# Post-deps local content tracking (F3)
# ------------------------------------------------------------------
old_local_deployed: list[str] = field(default_factory=list) # pipeline setup
local_deployed_files: list[str] = field(default_factory=list) # integrate (root)
local_content_errors_before: int = 0 # integrate (pre-root)
# ------------------------------------------------------------------
# Cowork integration state
# ------------------------------------------------------------------
cowork_nonsupported_warned: bool = False # integrate (once-per-run guard)
# ------------------------------------------------------------------
# TUI controller (PR #1116, workstream B): one Live region for the
# whole pipeline. Phases call ``ctx.tui.start_phase(...)`` /
# ``ctx.tui.task_started(...)`` / ``ctx.tui.task_completed(...)``;
# when the controller is disabled (CI, dumb terminal,
# ``APM_PROGRESS=never``) every method is a no-op. Pipeline owns
# the context-manager lifecycle (``with ctx.tui:``) so individual
# phases never need to enter / exit it.
# ------------------------------------------------------------------
tui: Any = None # InstallTui
# ------------------------------------------------------------------
# Legacy skill paths opt-out (convergence §3)
# ------------------------------------------------------------------
legacy_skill_paths: bool = False # --legacy-skill-paths flag or APM_LEGACY_SKILL_PATHS env