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,539 @@
|
|
|
1
|
+
"""Tier a1 — pure JavaScript / TypeScript surface parser.
|
|
2
|
+
|
|
3
|
+
Forge is a tier classifier, not a JS compiler. We extract the few things
|
|
4
|
+
the downstream pipeline needs from JS/TS source via regex:
|
|
5
|
+
|
|
6
|
+
* top-level imports (ES6 ``import … from "x"`` and CommonJS ``require("x")``)
|
|
7
|
+
* top-level exported symbols (``export const``, ``export function``,
|
|
8
|
+
``export class``, ``export default { fetch }`` Worker handlers)
|
|
9
|
+
* cheap state / effect signals (the presence of ``class``, ``let`` at
|
|
10
|
+
module level with subsequent reassignment, ``new`` constructors, and
|
|
11
|
+
Cloudflare-Worker ``fetch`` / ``scheduled`` entry points)
|
|
12
|
+
|
|
13
|
+
Block comments and string literals are stripped before parsing so we don't
|
|
14
|
+
treat ``"import x"`` inside a string as a real import. Single-line ``//``
|
|
15
|
+
comments are stripped too. The parser is forgiving — malformed sources
|
|
16
|
+
produce an empty surface rather than raising.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
# --- comment / string stripping -------------------------------------------
|
|
25
|
+
|
|
26
|
+
_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
27
|
+
_LINE_COMMENT_RE = re.compile(r"(?<!:)//[^\n]*")
|
|
28
|
+
_STRING_RE = re.compile(
|
|
29
|
+
r'"(?:\\.|[^"\\])*"'
|
|
30
|
+
r"|'(?:\\.|[^'\\])*'"
|
|
31
|
+
r"|`(?:\\.|[^`\\])*`",
|
|
32
|
+
re.DOTALL,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def strip_comments(src: str) -> str:
|
|
37
|
+
"""Remove block + line comments. Pure."""
|
|
38
|
+
src = _BLOCK_COMMENT_RE.sub("", src)
|
|
39
|
+
src = _LINE_COMMENT_RE.sub("", src)
|
|
40
|
+
return src
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def strip_comments_and_strings(src: str) -> str:
|
|
44
|
+
"""Remove block comments, line comments, and string literals.
|
|
45
|
+
|
|
46
|
+
Replaces strings with empty quotes so token positions are preserved
|
|
47
|
+
enough for line-based regex. Used for export / class / state checks
|
|
48
|
+
where the *contents* of strings are noise. Import parsers must use
|
|
49
|
+
:func:`strip_comments` instead so the import specifier itself
|
|
50
|
+
survives.
|
|
51
|
+
"""
|
|
52
|
+
return _STRING_RE.sub('""', strip_comments(src))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- imports --------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
# ES6 forms we handle:
|
|
58
|
+
# import "x"
|
|
59
|
+
# import x from "y"
|
|
60
|
+
# import { a, b } from "y"
|
|
61
|
+
# import * as ns from "y"
|
|
62
|
+
# import x, { a } from "y"
|
|
63
|
+
# import type { X } from "y" (TypeScript)
|
|
64
|
+
# import("x") ← dynamic import
|
|
65
|
+
_ES6_IMPORT_RE = re.compile(
|
|
66
|
+
r"""
|
|
67
|
+
\bimport\b
|
|
68
|
+
(?: # any clause shape (or none for side-effect)
|
|
69
|
+
\s+(?:type\s+)? # optional 'type' for TS
|
|
70
|
+
(?:[A-Za-z_$][\w$]*\s*,?\s*)? # default name
|
|
71
|
+
(?:\{[^}]*\}\s*)? # named-bindings { a, b }
|
|
72
|
+
(?:\*\s*as\s+[A-Za-z_$][\w$]*\s*)? # namespace
|
|
73
|
+
from\s*
|
|
74
|
+
)?
|
|
75
|
+
\s*
|
|
76
|
+
[\"']([^\"']+)[\"']
|
|
77
|
+
""",
|
|
78
|
+
re.VERBOSE,
|
|
79
|
+
)
|
|
80
|
+
_DYNAMIC_IMPORT_RE = re.compile(r"""\bimport\s*\(\s*[\"']([^\"']+)[\"']""")
|
|
81
|
+
_REQUIRE_RE = re.compile(r"""\brequire\s*\(\s*[\"']([^\"']+)[\"']""")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _mask_non_import_strings(src: str) -> str:
|
|
85
|
+
"""Mask string-literal *contents* unless they directly follow an
|
|
86
|
+
``import`` / ``from`` / ``require(`` token. Pure.
|
|
87
|
+
|
|
88
|
+
The masked output preserves length and quote positions so subsequent
|
|
89
|
+
regex matchers see the surrounding code unchanged.
|
|
90
|
+
"""
|
|
91
|
+
out: list[str] = []
|
|
92
|
+
i = 0
|
|
93
|
+
n = len(src)
|
|
94
|
+
keyword_re = re.compile(r"(?:^|[^A-Za-z0-9_$])(import|from|require)\s*\(?\s*$")
|
|
95
|
+
while i < n:
|
|
96
|
+
ch = src[i]
|
|
97
|
+
if ch in ("'", '"', "`"):
|
|
98
|
+
# find end of string
|
|
99
|
+
j = i + 1
|
|
100
|
+
while j < n:
|
|
101
|
+
if src[j] == "\\":
|
|
102
|
+
j += 2
|
|
103
|
+
continue
|
|
104
|
+
if src[j] == ch:
|
|
105
|
+
break
|
|
106
|
+
j += 1
|
|
107
|
+
# decide: was this string preceded by an import-context keyword?
|
|
108
|
+
preceding = src[max(0, i - 40): i]
|
|
109
|
+
preserve = bool(keyword_re.search(preceding))
|
|
110
|
+
if preserve:
|
|
111
|
+
out.append(src[i:j + 1])
|
|
112
|
+
else:
|
|
113
|
+
# mask: keep quote chars at the boundaries, blank middle
|
|
114
|
+
if j < n:
|
|
115
|
+
out.append(ch + " " * max(0, j - i - 1) + ch)
|
|
116
|
+
else:
|
|
117
|
+
out.append(ch + " " * (n - i - 1))
|
|
118
|
+
i = j + 1
|
|
119
|
+
continue
|
|
120
|
+
out.append(ch)
|
|
121
|
+
i += 1
|
|
122
|
+
return "".join(out)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_imports(src: str) -> list[str]:
|
|
126
|
+
"""Return every imported module specifier in source-order, deduped.
|
|
127
|
+
|
|
128
|
+
Detects ES6 ``import``, dynamic ``import()`` and CommonJS ``require()``.
|
|
129
|
+
Comments are stripped first; non-import string literals are masked so
|
|
130
|
+
a fake ``import x from 'y'`` inside a JS string never registers.
|
|
131
|
+
"""
|
|
132
|
+
cleaned = _mask_non_import_strings(strip_comments(src))
|
|
133
|
+
seen: list[str] = []
|
|
134
|
+
for rx in (_ES6_IMPORT_RE, _DYNAMIC_IMPORT_RE, _REQUIRE_RE):
|
|
135
|
+
for m in rx.finditer(cleaned):
|
|
136
|
+
spec = m.group(1)
|
|
137
|
+
if spec and spec not in seen:
|
|
138
|
+
seen.append(spec)
|
|
139
|
+
return seen
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- exports / symbols ----------------------------------------------------
|
|
143
|
+
|
|
144
|
+
_EXPORT_FUNCTION_RE = re.compile(
|
|
145
|
+
r"\bexport\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)"
|
|
146
|
+
)
|
|
147
|
+
_EXPORT_CLASS_RE = re.compile(r"\bexport\s+class\s+([A-Za-z_$][\w$]*)")
|
|
148
|
+
_EXPORT_CONST_RE = re.compile(r"\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)")
|
|
149
|
+
_EXPORT_DEFAULT_OBJECT_OPEN_RE = re.compile(r"\bexport\s+default\s*\{")
|
|
150
|
+
_EXPORT_DEFAULT_FUNCTION_RE = re.compile(
|
|
151
|
+
r"\bexport\s+default\s+(?:async\s+)?function(?:\s+([A-Za-z_$][\w$]*))?"
|
|
152
|
+
)
|
|
153
|
+
_EXPORT_DEFAULT_CLASS_RE = re.compile(
|
|
154
|
+
r"\bexport\s+default\s+class(?:\s+([A-Za-z_$][\w$]*))?"
|
|
155
|
+
)
|
|
156
|
+
# CommonJS: module.exports = { … } exports.foo = …
|
|
157
|
+
_MODULE_EXPORTS_OBJECT_RE = re.compile(
|
|
158
|
+
r"\bmodule\.exports\s*=\s*\{\s*([^}]{0,300})\}", re.DOTALL
|
|
159
|
+
)
|
|
160
|
+
_EXPORTS_PROPERTY_RE = re.compile(r"\bexports\.([A-Za-z_$][\w$]*)\s*=")
|
|
161
|
+
|
|
162
|
+
# Top-level (non-export) declarations we track for inferring tier.
|
|
163
|
+
_TOP_FUNCTION_RE = re.compile(
|
|
164
|
+
r"^(?:async\s+)?function\s+([A-Za-z_$][\w$]*)", re.MULTILINE
|
|
165
|
+
)
|
|
166
|
+
_TOP_CLASS_RE = re.compile(r"^class\s+([A-Za-z_$][\w$]*)", re.MULTILINE)
|
|
167
|
+
_TOP_CONST_RE = re.compile(
|
|
168
|
+
r"^(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=", re.MULTILINE
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class JsSurface:
|
|
174
|
+
"""The parsed surface of a single JS/TS file."""
|
|
175
|
+
|
|
176
|
+
imports: list[str] = field(default_factory=list)
|
|
177
|
+
exported_functions: list[str] = field(default_factory=list)
|
|
178
|
+
exported_classes: list[str] = field(default_factory=list)
|
|
179
|
+
exported_consts: list[str] = field(default_factory=list)
|
|
180
|
+
default_export_kind: str = "" # "" | "object" | "function" | "class"
|
|
181
|
+
default_export_keys: list[str] = field(default_factory=list)
|
|
182
|
+
has_class: bool = False
|
|
183
|
+
has_module_exports: bool = False
|
|
184
|
+
has_worker_default_fetch: bool = False
|
|
185
|
+
has_scheduled_handler: bool = False
|
|
186
|
+
top_level_consts: list[str] = field(default_factory=list)
|
|
187
|
+
top_level_functions: list[str] = field(default_factory=list)
|
|
188
|
+
top_level_classes: list[str] = field(default_factory=list)
|
|
189
|
+
statement_count: int = 0
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def all_exports(self) -> list[str]:
|
|
193
|
+
seen: list[str] = []
|
|
194
|
+
for name in (
|
|
195
|
+
self.exported_functions
|
|
196
|
+
+ self.exported_classes
|
|
197
|
+
+ self.exported_consts
|
|
198
|
+
+ self.default_export_keys
|
|
199
|
+
):
|
|
200
|
+
if name and name not in seen:
|
|
201
|
+
seen.append(name)
|
|
202
|
+
return seen
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _balance_braces(src: str, open_idx: int) -> int:
|
|
206
|
+
"""Return the index of the matching close brace for ``src[open_idx] == '{'``.
|
|
207
|
+
|
|
208
|
+
Brace-counts while skipping string literals and comments. Returns
|
|
209
|
+
``-1`` if no match is found (malformed source).
|
|
210
|
+
"""
|
|
211
|
+
if open_idx >= len(src) or src[open_idx] != "{":
|
|
212
|
+
return -1
|
|
213
|
+
depth = 0
|
|
214
|
+
i = open_idx
|
|
215
|
+
n = len(src)
|
|
216
|
+
while i < n:
|
|
217
|
+
ch = src[i]
|
|
218
|
+
if ch == "/" and i + 1 < n and src[i + 1] == "/":
|
|
219
|
+
j = src.find("\n", i)
|
|
220
|
+
i = n if j == -1 else j + 1
|
|
221
|
+
continue
|
|
222
|
+
if ch == "/" and i + 1 < n and src[i + 1] == "*":
|
|
223
|
+
j = src.find("*/", i + 2)
|
|
224
|
+
i = n if j == -1 else j + 2
|
|
225
|
+
continue
|
|
226
|
+
if ch in ("'", '"', "`"):
|
|
227
|
+
j = i + 1
|
|
228
|
+
while j < n:
|
|
229
|
+
if src[j] == "\\":
|
|
230
|
+
j += 2
|
|
231
|
+
continue
|
|
232
|
+
if src[j] == ch:
|
|
233
|
+
break
|
|
234
|
+
j += 1
|
|
235
|
+
i = j + 1
|
|
236
|
+
continue
|
|
237
|
+
if ch == "{":
|
|
238
|
+
depth += 1
|
|
239
|
+
elif ch == "}":
|
|
240
|
+
depth -= 1
|
|
241
|
+
if depth == 0:
|
|
242
|
+
return i
|
|
243
|
+
i += 1
|
|
244
|
+
return -1
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _top_level_object_keys(src: str, open_idx: int) -> list[str]:
|
|
248
|
+
"""Extract identifier keys at depth-1 of the object starting at ``{``.
|
|
249
|
+
|
|
250
|
+
Skips nested ``{}``, ``[]``, ``()`` blocks, comments and strings; reads
|
|
251
|
+
only the leading identifier of each property. Tolerant — any token it
|
|
252
|
+
doesn't recognise is silently skipped.
|
|
253
|
+
"""
|
|
254
|
+
close = _balance_braces(src, open_idx)
|
|
255
|
+
if close < 0:
|
|
256
|
+
return []
|
|
257
|
+
keys: list[str] = []
|
|
258
|
+
i = open_idx + 1
|
|
259
|
+
n = close
|
|
260
|
+
while i < n:
|
|
261
|
+
ch = src[i]
|
|
262
|
+
# skip whitespace / commas
|
|
263
|
+
if ch.isspace() or ch == ",":
|
|
264
|
+
i += 1
|
|
265
|
+
continue
|
|
266
|
+
# skip comments
|
|
267
|
+
if ch == "/" and i + 1 < n and src[i + 1] == "/":
|
|
268
|
+
j = src.find("\n", i)
|
|
269
|
+
i = n if j == -1 or j > n else j + 1
|
|
270
|
+
continue
|
|
271
|
+
if ch == "/" and i + 1 < n and src[i + 1] == "*":
|
|
272
|
+
j = src.find("*/", i + 2)
|
|
273
|
+
i = n if j == -1 or j > n else j + 2
|
|
274
|
+
continue
|
|
275
|
+
# spread / computed keys we don't try to parse
|
|
276
|
+
if ch in ("[", "."):
|
|
277
|
+
i += 1
|
|
278
|
+
continue
|
|
279
|
+
# quoted-string key — skip its value
|
|
280
|
+
if ch in ("'", '"', "`"):
|
|
281
|
+
i = _skip_string(src, i) + 1
|
|
282
|
+
continue
|
|
283
|
+
# identifier?
|
|
284
|
+
m = re.match(r"(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)",
|
|
285
|
+
src[i:n])
|
|
286
|
+
if not m:
|
|
287
|
+
i += 1
|
|
288
|
+
continue
|
|
289
|
+
name = m.group(1)
|
|
290
|
+
if name not in ("async", "get", "set") and name not in keys:
|
|
291
|
+
keys.append(name)
|
|
292
|
+
# skip past identifier
|
|
293
|
+
i += m.end()
|
|
294
|
+
# skip past the value: jump to next top-level comma
|
|
295
|
+
while i < n:
|
|
296
|
+
c2 = src[i]
|
|
297
|
+
if c2 == "{":
|
|
298
|
+
e = _balance_braces(src, i)
|
|
299
|
+
i = (e + 1) if e > i else (i + 1)
|
|
300
|
+
continue
|
|
301
|
+
if c2 == "[":
|
|
302
|
+
e = _balance_brackets(src, i)
|
|
303
|
+
i = (e + 1) if e > i else (i + 1)
|
|
304
|
+
continue
|
|
305
|
+
if c2 == "(":
|
|
306
|
+
e = _balance_parens(src, i)
|
|
307
|
+
i = (e + 1) if e > i else (i + 1)
|
|
308
|
+
continue
|
|
309
|
+
if c2 in ("'", '"', "`"):
|
|
310
|
+
i = _skip_string(src, i) + 1
|
|
311
|
+
continue
|
|
312
|
+
if c2 == "/" and i + 1 < n and src[i + 1] == "/":
|
|
313
|
+
j = src.find("\n", i)
|
|
314
|
+
i = n if j == -1 or j > n else j + 1
|
|
315
|
+
continue
|
|
316
|
+
if c2 == "/" and i + 1 < n and src[i + 1] == "*":
|
|
317
|
+
j = src.find("*/", i + 2)
|
|
318
|
+
i = n if j == -1 or j > n else j + 2
|
|
319
|
+
continue
|
|
320
|
+
if c2 == ",":
|
|
321
|
+
i += 1
|
|
322
|
+
break
|
|
323
|
+
i += 1
|
|
324
|
+
return keys
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _skip_string(src: str, i: int) -> int:
|
|
328
|
+
"""Return the index of the closing quote of a string literal at ``src[i]``."""
|
|
329
|
+
quote = src[i]
|
|
330
|
+
j = i + 1
|
|
331
|
+
n = len(src)
|
|
332
|
+
while j < n:
|
|
333
|
+
if src[j] == "\\":
|
|
334
|
+
j += 2
|
|
335
|
+
continue
|
|
336
|
+
if src[j] == quote:
|
|
337
|
+
return j
|
|
338
|
+
j += 1
|
|
339
|
+
return n - 1
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _balance_brackets(src: str, open_idx: int) -> int:
|
|
343
|
+
return _balance_pair(src, open_idx, "[", "]")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _balance_parens(src: str, open_idx: int) -> int:
|
|
347
|
+
return _balance_pair(src, open_idx, "(", ")")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _balance_pair(src: str, open_idx: int, opener: str, closer: str) -> int:
|
|
351
|
+
if open_idx >= len(src) or src[open_idx] != opener:
|
|
352
|
+
return -1
|
|
353
|
+
depth = 0
|
|
354
|
+
i = open_idx
|
|
355
|
+
n = len(src)
|
|
356
|
+
while i < n:
|
|
357
|
+
ch = src[i]
|
|
358
|
+
if ch in ("'", '"', "`"):
|
|
359
|
+
i = _skip_string(src, i) + 1
|
|
360
|
+
continue
|
|
361
|
+
if ch == opener:
|
|
362
|
+
depth += 1
|
|
363
|
+
elif ch == closer:
|
|
364
|
+
depth -= 1
|
|
365
|
+
if depth == 0:
|
|
366
|
+
return i
|
|
367
|
+
i += 1
|
|
368
|
+
return -1
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def parse_surface(src: str) -> JsSurface:
|
|
372
|
+
"""Return a ``JsSurface`` describing the file's exports + signals.
|
|
373
|
+
|
|
374
|
+
Forgiving: any regex misfire on malformed source produces an empty
|
|
375
|
+
field rather than raising. Block comments and string literals are
|
|
376
|
+
stripped first so we don't pick up imports inside docs.
|
|
377
|
+
"""
|
|
378
|
+
# Two views:
|
|
379
|
+
# ``cleaned`` — comments + string contents stripped (for surface regexes)
|
|
380
|
+
# ``no_comm`` — comments stripped only (for brace-walking, which needs
|
|
381
|
+
# the real braces)
|
|
382
|
+
cleaned = strip_comments_and_strings(src)
|
|
383
|
+
no_comm = strip_comments(src)
|
|
384
|
+
s = JsSurface()
|
|
385
|
+
|
|
386
|
+
s.imports = parse_imports(src)
|
|
387
|
+
s.exported_functions = _unique(_EXPORT_FUNCTION_RE.findall(cleaned))
|
|
388
|
+
s.exported_classes = _unique(_EXPORT_CLASS_RE.findall(cleaned))
|
|
389
|
+
s.exported_consts = _unique(_EXPORT_CONST_RE.findall(cleaned))
|
|
390
|
+
|
|
391
|
+
default_obj = _EXPORT_DEFAULT_OBJECT_OPEN_RE.search(no_comm)
|
|
392
|
+
default_fn = _EXPORT_DEFAULT_FUNCTION_RE.search(cleaned)
|
|
393
|
+
default_cls = _EXPORT_DEFAULT_CLASS_RE.search(cleaned)
|
|
394
|
+
if default_obj:
|
|
395
|
+
s.default_export_kind = "object"
|
|
396
|
+
# The match ends just past the opening ``{`` — step back one to point
|
|
397
|
+
# at the brace.
|
|
398
|
+
open_idx = default_obj.end() - 1
|
|
399
|
+
s.default_export_keys = _top_level_object_keys(no_comm, open_idx)
|
|
400
|
+
if "fetch" in s.default_export_keys:
|
|
401
|
+
s.has_worker_default_fetch = True
|
|
402
|
+
if "scheduled" in s.default_export_keys:
|
|
403
|
+
s.has_scheduled_handler = True
|
|
404
|
+
elif default_fn:
|
|
405
|
+
s.default_export_kind = "function"
|
|
406
|
+
if default_fn.group(1):
|
|
407
|
+
s.default_export_keys = [default_fn.group(1)]
|
|
408
|
+
elif default_cls:
|
|
409
|
+
s.default_export_kind = "class"
|
|
410
|
+
if default_cls.group(1):
|
|
411
|
+
s.default_export_keys = [default_cls.group(1)]
|
|
412
|
+
|
|
413
|
+
if _MODULE_EXPORTS_OBJECT_RE.search(cleaned):
|
|
414
|
+
s.has_module_exports = True
|
|
415
|
+
s.exported_consts.extend(
|
|
416
|
+
n for n in _EXPORTS_PROPERTY_RE.findall(cleaned)
|
|
417
|
+
if n not in s.exported_consts
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
s.top_level_functions = _unique(_TOP_FUNCTION_RE.findall(cleaned))
|
|
421
|
+
s.top_level_classes = _unique(_TOP_CLASS_RE.findall(cleaned))
|
|
422
|
+
s.top_level_consts = _unique(_TOP_CONST_RE.findall(cleaned))
|
|
423
|
+
|
|
424
|
+
s.has_class = bool(s.top_level_classes or s.exported_classes)
|
|
425
|
+
if not s.has_scheduled_handler and re.search(r"\bscheduled\s*\(", cleaned):
|
|
426
|
+
s.has_scheduled_handler = True
|
|
427
|
+
|
|
428
|
+
s.statement_count = sum(1 for line in cleaned.splitlines() if line.strip())
|
|
429
|
+
return s
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _unique(items: list[str]) -> list[str]:
|
|
433
|
+
seen: list[str] = []
|
|
434
|
+
for x in items:
|
|
435
|
+
if x and x not in seen:
|
|
436
|
+
seen.append(x)
|
|
437
|
+
return seen
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# --- tier classification --------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def classify_js_tier(*, path: str, surface: JsSurface) -> str:
|
|
443
|
+
"""Return the canonical tier directory for a JS/TS file.
|
|
444
|
+
|
|
445
|
+
Honours an explicit ``aN_*`` directory placement first; otherwise infers
|
|
446
|
+
from the parsed surface:
|
|
447
|
+
|
|
448
|
+
* ``a4_sy_orchestration`` — Cloudflare Worker default ``{ fetch }``,
|
|
449
|
+
scheduled handlers, top-level ``server.listen``, or a CLI shebang.
|
|
450
|
+
* ``a0_qk_constants`` — only ``export const`` declarations, no
|
|
451
|
+
functions, no classes.
|
|
452
|
+
* ``a2_mo_composites`` — has a class or holds module-level state.
|
|
453
|
+
* ``a3_og_features`` — directory hint or feature-flavoured name.
|
|
454
|
+
* ``a1_at_functions`` — pure function module (default).
|
|
455
|
+
"""
|
|
456
|
+
norm = path.replace("\\", "/").lower()
|
|
457
|
+
for tier in (
|
|
458
|
+
"a0_qk_constants",
|
|
459
|
+
"a1_at_functions",
|
|
460
|
+
"a2_mo_composites",
|
|
461
|
+
"a3_og_features",
|
|
462
|
+
"a4_sy_orchestration",
|
|
463
|
+
):
|
|
464
|
+
if f"/{tier}/" in f"/{norm}/" or norm.startswith(f"{tier}/"):
|
|
465
|
+
return tier
|
|
466
|
+
|
|
467
|
+
if surface.has_worker_default_fetch or surface.has_scheduled_handler:
|
|
468
|
+
return "a4_sy_orchestration"
|
|
469
|
+
|
|
470
|
+
name = norm.rsplit("/", 1)[-1]
|
|
471
|
+
if (
|
|
472
|
+
name.endswith(("_main.js", "_cli.js", "_runner.js", "_server.js",
|
|
473
|
+
"_main.ts", "_cli.ts", "_runner.ts", "_server.ts"))
|
|
474
|
+
or name in {"server.js", "main.js", "index.js", "worker.js",
|
|
475
|
+
"server.ts", "main.ts", "index.ts", "worker.ts"}
|
|
476
|
+
or "/bin/" in f"/{norm}"
|
|
477
|
+
):
|
|
478
|
+
# index.js / main.ts often *are* the top entry point of a package
|
|
479
|
+
return "a4_sy_orchestration"
|
|
480
|
+
|
|
481
|
+
only_consts = (
|
|
482
|
+
surface.exported_consts
|
|
483
|
+
and not surface.exported_functions
|
|
484
|
+
and not surface.exported_classes
|
|
485
|
+
and not surface.top_level_functions
|
|
486
|
+
and not surface.top_level_classes
|
|
487
|
+
and not surface.default_export_kind
|
|
488
|
+
)
|
|
489
|
+
if only_consts:
|
|
490
|
+
return "a0_qk_constants"
|
|
491
|
+
|
|
492
|
+
if surface.has_class:
|
|
493
|
+
return "a2_mo_composites"
|
|
494
|
+
|
|
495
|
+
if any(tag in name for tag in ("_feature", "_pipeline", "_gate", "_service")):
|
|
496
|
+
return "a3_og_features"
|
|
497
|
+
|
|
498
|
+
if any(
|
|
499
|
+
tag in name
|
|
500
|
+
for tag in ("_client", "_store", "_registry", "_core", "_state")
|
|
501
|
+
):
|
|
502
|
+
return "a2_mo_composites"
|
|
503
|
+
|
|
504
|
+
if any(
|
|
505
|
+
tag in name
|
|
506
|
+
for tag in ("_utils", "_helpers", "_validators", "_parsers",
|
|
507
|
+
"_format", "_formatter")
|
|
508
|
+
):
|
|
509
|
+
return "a1_at_functions"
|
|
510
|
+
|
|
511
|
+
return "a1_at_functions"
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# --- effect inference -----------------------------------------------------
|
|
515
|
+
|
|
516
|
+
_NETWORK_RE = re.compile(
|
|
517
|
+
r"\b(fetch|XMLHttpRequest|WebSocket|http\.request|https\.request)\b"
|
|
518
|
+
)
|
|
519
|
+
_FS_RE = re.compile(
|
|
520
|
+
r"\b(fs\.(?:read|write|append|unlink|mkdir)|require\(['\"]fs['\"]\))"
|
|
521
|
+
)
|
|
522
|
+
_STATE_RE = re.compile(r"\b(let|var)\b")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def detect_js_effects(src: str) -> list[str]:
|
|
526
|
+
"""Cheap effect inference. Same shape as the Python ``detect_effects``."""
|
|
527
|
+
cleaned = strip_comments_and_strings(src)
|
|
528
|
+
effects: list[str] = []
|
|
529
|
+
if _NETWORK_RE.search(cleaned) or _FS_RE.search(cleaned):
|
|
530
|
+
effects.append("io")
|
|
531
|
+
if "class " in cleaned or _STATE_RE.search(cleaned):
|
|
532
|
+
effects.append("state")
|
|
533
|
+
if not effects:
|
|
534
|
+
return ["pure"]
|
|
535
|
+
seen: list[str] = []
|
|
536
|
+
for e in effects:
|
|
537
|
+
if e not in seen:
|
|
538
|
+
seen.append(e)
|
|
539
|
+
return seen
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Tier a1 — pure Vanguard-style lineage chain primitives.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W4 deliverable. Until AAAA-Nexus
|
|
4
|
+
``/v1/forge/lineage`` ships, every Receipt still gets a real
|
|
5
|
+
``lineage`` block via a local content-addressed chain:
|
|
6
|
+
|
|
7
|
+
receipt_hash := SHA-256(canonical_json(receipt minus mutable fields))
|
|
8
|
+
link_n.parent_receipt_hash = receipt_hash(receipt_{n-1})
|
|
9
|
+
link_n.chain_depth = link_{n-1}.chain_depth + 1
|
|
10
|
+
link_n.lineage_path = 'local://lineage-chain/<receipt_hash>'
|
|
11
|
+
|
|
12
|
+
Mutable fields excluded from the canonical hash:
|
|
13
|
+
signatures — added by Lane A W2 signer (would change the hash if
|
|
14
|
+
we hashed AFTER signing)
|
|
15
|
+
lineage — circular: hashing the lineage block would require
|
|
16
|
+
the hash to depend on itself
|
|
17
|
+
notes — soft-fail strings appended over time
|
|
18
|
+
generated_at_utc — timestamp drift between emit and sign
|
|
19
|
+
|
|
20
|
+
Once Vanguard ships, ``a2_mo_composites.lineage_chain_store`` will
|
|
21
|
+
also POST each link to ``/v1/forge/lineage`` with the same soft-fail
|
|
22
|
+
contract as W2 signer; the LOCAL chain remains the source of truth.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
from copy import deepcopy
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from ..a0_qk_constants.receipt_schema import ForgeReceiptV1, ReceiptLineage
|
|
32
|
+
|
|
33
|
+
_HASH_EXCLUDE_FIELDS: frozenset[str] = frozenset({
|
|
34
|
+
"signatures",
|
|
35
|
+
"lineage",
|
|
36
|
+
"notes",
|
|
37
|
+
"generated_at_utc",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
_LOCAL_LINEAGE_PREFIX: str = "local://lineage-chain/"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def canonical_receipt_hash(receipt: ForgeReceiptV1) -> str:
|
|
44
|
+
"""SHA-256 hex digest of the receipt's structural content.
|
|
45
|
+
|
|
46
|
+
Excludes the four mutable fields enumerated in
|
|
47
|
+
``_HASH_EXCLUDE_FIELDS`` so signing / re-emitting / re-noting a
|
|
48
|
+
receipt does not change its identity. Two receipts with the same
|
|
49
|
+
project + verdict + certify + wire + scout content yield the same
|
|
50
|
+
hash regardless of sign state.
|
|
51
|
+
"""
|
|
52
|
+
stripped: dict[str, Any] = {
|
|
53
|
+
k: v for k, v in receipt.items()
|
|
54
|
+
if k not in _HASH_EXCLUDE_FIELDS
|
|
55
|
+
}
|
|
56
|
+
canonical = json.dumps(stripped, sort_keys=True, default=str)
|
|
57
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def link_to_parent(
|
|
61
|
+
receipt: ForgeReceiptV1,
|
|
62
|
+
*,
|
|
63
|
+
parent_receipt_hash: str | None,
|
|
64
|
+
chain_depth: int,
|
|
65
|
+
lineage_path: str | None = None,
|
|
66
|
+
) -> ForgeReceiptV1:
|
|
67
|
+
"""Return a deep-copy of ``receipt`` with the lineage block populated.
|
|
68
|
+
|
|
69
|
+
``parent_receipt_hash``: SHA-256 hex digest of the immediately
|
|
70
|
+
prior link's receipt (None for chain head).
|
|
71
|
+
``chain_depth``: 1 for the chain head; n+1 for each link.
|
|
72
|
+
``lineage_path``: optional override; defaults to a local URI
|
|
73
|
+
of the form ``local://lineage-chain/<hash>``.
|
|
74
|
+
|
|
75
|
+
Pure: input is not mutated.
|
|
76
|
+
"""
|
|
77
|
+
if chain_depth < 1:
|
|
78
|
+
raise ValueError("chain_depth must be >= 1")
|
|
79
|
+
out = deepcopy(receipt)
|
|
80
|
+
own_hash = canonical_receipt_hash(out)
|
|
81
|
+
path = lineage_path or f"{_LOCAL_LINEAGE_PREFIX}{own_hash}"
|
|
82
|
+
out["lineage"] = ReceiptLineage(
|
|
83
|
+
lineage_path=path,
|
|
84
|
+
parent_receipt_hash=parent_receipt_hash,
|
|
85
|
+
chain_depth=chain_depth,
|
|
86
|
+
)
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_local_lineage(lineage_path: str | None) -> bool:
|
|
91
|
+
"""True when the lineage_path is a local-chain pointer.
|
|
92
|
+
|
|
93
|
+
Useful for downstream tools (CS-1 PDF, Lane B Studio) to render a
|
|
94
|
+
'local-only' badge until the AAAA-Nexus Vanguard wire-up ships.
|
|
95
|
+
"""
|
|
96
|
+
return bool(lineage_path) and str(lineage_path).startswith(_LOCAL_LINEAGE_PREFIX)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def verify_chain_link(
|
|
100
|
+
child: ForgeReceiptV1,
|
|
101
|
+
parent: ForgeReceiptV1 | None,
|
|
102
|
+
) -> tuple[bool, list[str]]:
|
|
103
|
+
"""Verify that ``child`` is a valid successor of ``parent``.
|
|
104
|
+
|
|
105
|
+
Returns (ok, problems). ``ok`` is True when:
|
|
106
|
+
* child.lineage.parent_receipt_hash == hash(parent) (or both None)
|
|
107
|
+
* child.lineage.chain_depth == parent.lineage.chain_depth + 1 (or 1)
|
|
108
|
+
|
|
109
|
+
Pure check; no I/O. Used by the chain store and by anyone who
|
|
110
|
+
wants to audit a saved chain offline.
|
|
111
|
+
"""
|
|
112
|
+
problems: list[str] = []
|
|
113
|
+
lineage = child.get("lineage") or {}
|
|
114
|
+
if not lineage:
|
|
115
|
+
problems.append("child receipt has no lineage block")
|
|
116
|
+
return False, problems
|
|
117
|
+
declared_parent = lineage.get("parent_receipt_hash")
|
|
118
|
+
declared_depth = lineage.get("chain_depth")
|
|
119
|
+
|
|
120
|
+
if parent is None:
|
|
121
|
+
if declared_parent is not None:
|
|
122
|
+
problems.append(
|
|
123
|
+
"chain head must have parent_receipt_hash=None; "
|
|
124
|
+
f"got {declared_parent!r}"
|
|
125
|
+
)
|
|
126
|
+
if declared_depth != 1:
|
|
127
|
+
problems.append(
|
|
128
|
+
f"chain head must have chain_depth=1; got {declared_depth!r}"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
expected_parent_hash = canonical_receipt_hash(parent)
|
|
132
|
+
if declared_parent != expected_parent_hash:
|
|
133
|
+
problems.append(
|
|
134
|
+
f"parent_receipt_hash mismatch: declared={declared_parent!r} "
|
|
135
|
+
f"expected={expected_parent_hash!r}"
|
|
136
|
+
)
|
|
137
|
+
parent_lineage = parent.get("lineage") or {}
|
|
138
|
+
parent_depth = int(parent_lineage.get("chain_depth", 0))
|
|
139
|
+
if declared_depth != parent_depth + 1:
|
|
140
|
+
problems.append(
|
|
141
|
+
f"chain_depth not monotonic: declared={declared_depth!r} "
|
|
142
|
+
f"parent_depth={parent_depth!r}"
|
|
143
|
+
)
|
|
144
|
+
return (not problems), problems
|