chipgraph 0.1.0__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.
Files changed (252) hide show
  1. chipgraph/__init__.py +7 -0
  2. chipgraph/adapters/__init__.py +1 -0
  3. chipgraph/adapters/format/__init__.py +6 -0
  4. chipgraph/adapters/format/__main__.py +16 -0
  5. chipgraph/adapters/format/_yaml_lines.py +54 -0
  6. chipgraph/adapters/format/chip_yaml.py +323 -0
  7. chipgraph/adapters/format/qsoc_contract.py +246 -0
  8. chipgraph/adapters/llm/__init__.py +58 -0
  9. chipgraph/adapters/llm/_budget.py +173 -0
  10. chipgraph/adapters/llm/_errors.py +50 -0
  11. chipgraph/adapters/llm/_nda.py +30 -0
  12. chipgraph/adapters/llm/_retry.py +85 -0
  13. chipgraph/adapters/llm/anthropic.py +247 -0
  14. chipgraph/adapters/llm/decide_backend.py +179 -0
  15. chipgraph/adapters/llm/fake.py +87 -0
  16. chipgraph/adapters/llm/guard.py +68 -0
  17. chipgraph/adapters/parser/__init__.py +22 -0
  18. chipgraph/adapters/parser/cocotb.py +304 -0
  19. chipgraph/adapters/parser/generic_regex.py +75 -0
  20. chipgraph/adapters/parser/verible.py +55 -0
  21. chipgraph/adapters/parser/verilator.py +46 -0
  22. chipgraph/adapters/review/__init__.py +7 -0
  23. chipgraph/adapters/review/file.py +123 -0
  24. chipgraph/adapters/review/github.py +886 -0
  25. chipgraph/adapters/runner/__init__.py +7 -0
  26. chipgraph/adapters/runner/local.py +122 -0
  27. chipgraph/adapters/runtime/__init__.py +5 -0
  28. chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
  29. chipgraph/adapters/runtime/claude_code/agents.py +158 -0
  30. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  31. chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
  32. chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
  33. chipgraph/adapters/runtime/claude_code/loop.py +342 -0
  34. chipgraph/adapters/runtime/claude_code/previous.py +419 -0
  35. chipgraph/adapters/runtime/claude_code/review.py +492 -0
  36. chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
  37. chipgraph/adapters/runtime/claude_code/service.py +2285 -0
  38. chipgraph/adapters/tool/__init__.py +7 -0
  39. chipgraph/adapters/tool/cmd.py +229 -0
  40. chipgraph/adapters/tool/edalize.py +773 -0
  41. chipgraph/adapters/tool/filelist.py +259 -0
  42. chipgraph/adapters/tool/pyslang.py +485 -0
  43. chipgraph/adapters/vcs/__init__.py +1 -0
  44. chipgraph/adapters/vcs/git.py +521 -0
  45. chipgraph/app/__init__.py +10 -0
  46. chipgraph/app/baseline.py +339 -0
  47. chipgraph/app/build.py +332 -0
  48. chipgraph/app/checks.py +202 -0
  49. chipgraph/app/context.py +191 -0
  50. chipgraph/app/errors.py +11 -0
  51. chipgraph/app/executors.py +79 -0
  52. chipgraph/app/findings.py +203 -0
  53. chipgraph/app/ingest.py +523 -0
  54. chipgraph/checks/__init__.py +52 -0
  55. chipgraph/checks/_cdc_analyze.py +272 -0
  56. chipgraph/checks/_cdc_elaborate.py +455 -0
  57. chipgraph/checks/_common.py +268 -0
  58. chipgraph/checks/_cross.py +96 -0
  59. chipgraph/checks/_model.py +64 -0
  60. chipgraph/checks/_naming_pyslang.py +235 -0
  61. chipgraph/checks/_naming_rules.py +127 -0
  62. chipgraph/checks/_xref_syntax.py +260 -0
  63. chipgraph/checks/cdc_struct.py +235 -0
  64. chipgraph/checks/connect.py +353 -0
  65. chipgraph/checks/cross_chip.py +454 -0
  66. chipgraph/checks/duplicate.py +153 -0
  67. chipgraph/checks/filelist.py +323 -0
  68. chipgraph/checks/format.py +268 -0
  69. chipgraph/checks/generated.py +151 -0
  70. chipgraph/checks/hardcode.py +233 -0
  71. chipgraph/checks/layout.py +155 -0
  72. chipgraph/checks/naming.py +311 -0
  73. chipgraph/checks/plan_check.py +901 -0
  74. chipgraph/checks/ports_diff.py +203 -0
  75. chipgraph/checks/spec_schema.py +277 -0
  76. chipgraph/checks/style.py +910 -0
  77. chipgraph/checks/tb_static.py +599 -0
  78. chipgraph/checks/trace.py +224 -0
  79. chipgraph/checks/vendor_guard.py +369 -0
  80. chipgraph/checks/wrap_check.py +486 -0
  81. chipgraph/cli/__init__.py +1688 -0
  82. chipgraph/cli/next_steps.py +137 -0
  83. chipgraph/core/__init__.py +1 -0
  84. chipgraph/core/config/__init__.py +93 -0
  85. chipgraph/core/config/defaults.py +14 -0
  86. chipgraph/core/config/errors.py +32 -0
  87. chipgraph/core/config/loader.py +766 -0
  88. chipgraph/core/config/models.py +845 -0
  89. chipgraph/core/config/templates.py +340 -0
  90. chipgraph/core/contracts/__init__.py +75 -0
  91. chipgraph/core/contracts/_paths.py +26 -0
  92. chipgraph/core/contracts/agent.py +49 -0
  93. chipgraph/core/contracts/artifact.py +68 -0
  94. chipgraph/core/contracts/check.py +76 -0
  95. chipgraph/core/contracts/event.py +70 -0
  96. chipgraph/core/contracts/export.py +128 -0
  97. chipgraph/core/contracts/finding.py +171 -0
  98. chipgraph/core/contracts/gate.py +50 -0
  99. chipgraph/core/contracts/rule.py +177 -0
  100. chipgraph/core/contracts/types.py +45 -0
  101. chipgraph/core/engine/__init__.py +70 -0
  102. chipgraph/core/engine/agent_rule.py +1362 -0
  103. chipgraph/core/engine/baseline.py +283 -0
  104. chipgraph/core/engine/decide.py +462 -0
  105. chipgraph/core/engine/fanout.py +1358 -0
  106. chipgraph/core/engine/gate.py +170 -0
  107. chipgraph/core/engine/graph.py +647 -0
  108. chipgraph/core/engine/records.py +60 -0
  109. chipgraph/core/engine/rules.py +92 -0
  110. chipgraph/core/engine/scheduler.py +637 -0
  111. chipgraph/core/engine/select.py +1239 -0
  112. chipgraph/core/engine/targets.py +342 -0
  113. chipgraph/core/model/__init__.py +98 -0
  114. chipgraph/core/model/entities.py +244 -0
  115. chipgraph/core/model/export.py +16 -0
  116. chipgraph/core/model/ingest.py +693 -0
  117. chipgraph/core/model/json_value.py +4 -0
  118. chipgraph/core/model/keys.py +95 -0
  119. chipgraph/core/model/model.py +153 -0
  120. chipgraph/core/model/provenance.py +27 -0
  121. chipgraph/core/model/query.py +583 -0
  122. chipgraph/core/model/registry.py +80 -0
  123. chipgraph/core/model/relations.py +61 -0
  124. chipgraph/core/model/store.py +279 -0
  125. chipgraph/core/plugin_api/__init__.py +81 -0
  126. chipgraph/core/plugin_api/local.py +167 -0
  127. chipgraph/core/plugin_api/pack.py +159 -0
  128. chipgraph/core/plugin_api/protocols.py +177 -0
  129. chipgraph/core/plugin_api/registry.py +111 -0
  130. chipgraph/core/plugin_api/types.py +94 -0
  131. chipgraph/core/runtime/__init__.py +41 -0
  132. chipgraph/core/runtime/executor.py +79 -0
  133. chipgraph/core/runtime/queue.py +565 -0
  134. chipgraph/core/runtime/roles/__init__.py +91 -0
  135. chipgraph/core/runtime/roles/data/author.md +41 -0
  136. chipgraph/core/runtime/roles/data/critic.md +56 -0
  137. chipgraph/core/runtime/roles/data/planner.md +38 -0
  138. chipgraph/core/runtime/roles/data/researcher.md +37 -0
  139. chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
  140. chipgraph/core/runtime/roles/data/tb-author.md +48 -0
  141. chipgraph/core/runtime/roles/data/triage.md +34 -0
  142. chipgraph/core/runtime/roles/policy.py +299 -0
  143. chipgraph/core/runtime/roles/registry.py +52 -0
  144. chipgraph/core/runtime/roles/review.py +275 -0
  145. chipgraph/core/runtime/roles/schema.py +82 -0
  146. chipgraph/core/runtime/roles/skills.py +134 -0
  147. chipgraph/core/runtime/roles/spec.py +232 -0
  148. chipgraph/core/state/__init__.py +1 -0
  149. chipgraph/core/state/artifacts.py +156 -0
  150. chipgraph/core/state/backend.py +117 -0
  151. chipgraph/core/state/findings.py +151 -0
  152. chipgraph/core/state/handoff.py +411 -0
  153. chipgraph/core/state/idempotency.py +59 -0
  154. chipgraph/core/state/journal.py +238 -0
  155. chipgraph/core/state/layout.py +82 -0
  156. chipgraph/core/state/lock.py +195 -0
  157. chipgraph/core/state/trace.py +225 -0
  158. chipgraph/learn/__init__.py +96 -0
  159. chipgraph/learn/draft.py +187 -0
  160. chipgraph/learn/infer.py +580 -0
  161. chipgraph/learn/models.py +145 -0
  162. chipgraph/learn/try_run.py +228 -0
  163. chipgraph/mcp/__init__.py +12 -0
  164. chipgraph/mcp/model_tools.py +167 -0
  165. chipgraph/mcp/server.py +787 -0
  166. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  167. chipgraph/orgs/qnsc/profile.yml +5 -0
  168. chipgraph/packs/__init__.py +8 -0
  169. chipgraph/packs/assist/__init__.py +16 -0
  170. chipgraph/packs/assist/ask/__init__.py +58 -0
  171. chipgraph/packs/assist/ask/_db.py +57 -0
  172. chipgraph/packs/assist/ask/_project.py +230 -0
  173. chipgraph/packs/assist/ask/answer.py +266 -0
  174. chipgraph/packs/assist/ask/check.py +182 -0
  175. chipgraph/packs/assist/ask/contract.py +115 -0
  176. chipgraph/packs/assist/ask/documents.py +157 -0
  177. chipgraph/packs/assist/ask/fts.py +80 -0
  178. chipgraph/packs/assist/ask/retrieve.py +331 -0
  179. chipgraph/packs/assist/audit/__init__.py +383 -0
  180. chipgraph/packs/assist/pack.yml +13 -0
  181. chipgraph/packs/assist/triage/__init__.py +64 -0
  182. chipgraph/packs/assist/triage/contract.py +94 -0
  183. chipgraph/packs/assist/triage/parse.py +371 -0
  184. chipgraph/packs/assist/triage/question.py +384 -0
  185. chipgraph/packs/assist/triage/report.py +160 -0
  186. chipgraph/packs/assist/triage/rules.py +447 -0
  187. chipgraph/packs/assist/triage/run.py +224 -0
  188. chipgraph/packs/digital_rtl/__init__.py +19 -0
  189. chipgraph/packs/digital_rtl/layout.py +144 -0
  190. chipgraph/packs/digital_rtl/pack.yml +25 -0
  191. chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
  192. chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
  193. chipgraph/packs/digital_rtl/plan/context.py +82 -0
  194. chipgraph/packs/digital_rtl/plan/expand.py +94 -0
  195. chipgraph/packs/digital_rtl/plan/model.py +150 -0
  196. chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
  197. chipgraph/packs/digital_rtl/plan/show.py +176 -0
  198. chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
  199. chipgraph/packs/digital_rtl/pr/body.py +165 -0
  200. chipgraph/packs/digital_rtl/pr/gate.py +110 -0
  201. chipgraph/packs/digital_rtl/pr/open.py +397 -0
  202. chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
  203. chipgraph/packs/digital_rtl/rtl/context.py +272 -0
  204. chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
  205. chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
  206. chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
  207. chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
  208. chipgraph/packs/digital_rtl/rules/review.yml +31 -0
  209. chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
  210. chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
  211. chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
  212. chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
  213. chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
  214. chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
  215. chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
  216. chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
  217. chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
  218. chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
  219. chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
  220. chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
  221. chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
  222. chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
  223. chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
  224. chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
  225. chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
  226. chipgraph/packs/dv/__init__.py +15 -0
  227. chipgraph/packs/dv/declaration.py +392 -0
  228. chipgraph/packs/dv/interface.py +596 -0
  229. chipgraph/packs/dv/pack.yml +14 -0
  230. chipgraph/packs/dv/rules/tb_module.yml +42 -0
  231. chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
  232. chipgraph/packs/spec_core/__init__.py +9 -0
  233. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  234. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  235. chipgraph/packs/spec_core/extract/_text.py +116 -0
  236. chipgraph/packs/spec_core/extract/mas.py +975 -0
  237. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  238. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  239. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  240. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  241. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  242. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  243. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  244. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  245. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  246. chipgraph/packs/spec_core/pack.yml +6 -0
  247. chipgraph/py.typed +0 -0
  248. chipgraph-0.1.0.dist-info/METADATA +53 -0
  249. chipgraph-0.1.0.dist-info/RECORD +252 -0
  250. chipgraph-0.1.0.dist-info/WHEEL +4 -0
  251. chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
  252. chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,11 @@
1
+ """Deterministic generators for the ``spec-core`` pack.
2
+
3
+ Currently the diagram generator,
4
+ :class:`~chipgraph.packs.spec_core.gen.diagram.DiagramGenerator`, which renders block,
5
+ memory-map, interrupt-map and clock/reset-tree figures from the Design Model as SVG and
6
+ drawio. Figures are computed from the model, never drawn by an AI (DESIGN.md 4.6).
7
+ """
8
+
9
+ from chipgraph.packs.spec_core.gen.diagram import DiagramGenerator, render
10
+
11
+ __all__ = ["DiagramGenerator", "render"]
@@ -0,0 +1,148 @@
1
+ """The ``agents-md`` generator (pack ``spec-core``): the chipgraph part of an `AGENTS.md`.
2
+
3
+ Optional and deterministic, no AI (DESIGN.md 4.7, DECISIONS D18/D31). From the resolved
4
+ profile and the organization rules it extends, it writes the project's conventions for
5
+ any coding agent (Codex, Cursor, Claude Code ...): blocks and instances, where files go,
6
+ naming rules, the checks to run before a PR, requirement IDs, path exceptions, data labels
7
+ and generated files.
8
+
9
+ chipgraph manages only the text between ``<!-- chipgraph:begin -->`` and
10
+ ``<!-- chipgraph:end -->``; the rest of the file is the team's and is never changed.
11
+
12
+ Public API:
13
+
14
+ - :func:`render_agents_md` — the managed block's content (without the markers).
15
+ Deterministic: sections sorted, no dates, no absolute paths, ``\\n`` line endings.
16
+ - :func:`update_agents_md` — put a block into an existing file's managed part (or append
17
+ it, or return it alone when there is no file); malformed markers raise
18
+ :class:`AgentsMdError`.
19
+ - :func:`agents_md_content` — both in one: the full file content to write.
20
+
21
+ The block is rendered from the Jinja2 template ``agents_md.md.j2`` through
22
+ :class:`~chipgraph.core.config.templates.TemplateSet` (DESIGN 8.6 V2), so a project can
23
+ replace one section without copying the file: ``templates.override`` in `.chipgraph.yml`
24
+ maps a block name to a file holding that one ``{% block %}``, e.g.
25
+ ``templates: { override: { agents_md_checks: templates/agents_checks.j2 } }``. Block
26
+ names: ``agents_md_intro``, ``agents_md_project``, ``agents_md_layout``,
27
+ ``agents_md_naming``, ``agents_md_checks``, ``agents_md_requirements``,
28
+ ``agents_md_paths``, ``agents_md_data``, ``agents_md_generated``. An override may use the
29
+ same context as the pack template, plus the helpers ``code(s, cell=False)`` (a Markdown
30
+ code span) and ``text(s)`` (one line of text, safe in a table cell). Only overrides named
31
+ ``agents_md.md.j2`` or starting with ``agents_md_`` are read here.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ from pathlib import Path
38
+
39
+ from chipgraph.core.config import ResolvedProfile
40
+ from chipgraph.core.config.templates import TemplateLayer, TemplateSet, TemplateSetError
41
+ from chipgraph.packs.spec_core.gen.agents_md._context import build_context
42
+ from chipgraph.packs.spec_core.gen.agents_md._markers import (
43
+ BEGIN_MARKER,
44
+ END_MARKER,
45
+ AgentsMdError,
46
+ split_managed,
47
+ update_agents_md,
48
+ )
49
+
50
+ TEMPLATE_NAME = "agents_md.md.j2"
51
+ """The pack template's file name (and the key of a whole-file override)."""
52
+
53
+ BLOCK_PREFIX = "agents_md_"
54
+ """Every block of the template is named with this prefix."""
55
+
56
+ NOTE = (
57
+ "<!-- Generated by chipgraph from the project profile; do not edit this block by hand. "
58
+ "Regenerate it with `chipgraph agents-md --write`. -->"
59
+ )
60
+ """The one-line note that opens the managed block."""
61
+
62
+ _TEMPLATE_DIR = Path(__file__).resolve().parent
63
+
64
+ _BLANK_RUNS = re.compile(r"\n{3,}")
65
+
66
+
67
+ def code(value: object, cell: bool = False) -> str:
68
+ """`value` as a Markdown code span; with `cell`, `|` is escaped for a table cell."""
69
+ s = " ".join(str(value).split())
70
+ if not s:
71
+ return ""
72
+ fence = "``" if "`" in s else "`"
73
+ pad = " " if fence == "``" else ""
74
+ if cell:
75
+ s = s.replace("|", "\\|")
76
+ return f"{fence}{pad}{s}{pad}{fence}"
77
+
78
+
79
+ def text(value: object) -> str:
80
+ """`value` as one line of Markdown text, safe in a table cell.
81
+
82
+ Only `<` (else read as an HTML tag, e.g. ``m_qnsc_<function>``) and `|` (a cell
83
+ separator) are escaped; other Markdown in the value is kept as written.
84
+ """
85
+ s = " ".join(str(value).split())
86
+ for ch in ("<", "|"):
87
+ s = s.replace(ch, "\\" + ch)
88
+ return s
89
+
90
+
91
+ def _normalize(rendered: str) -> str:
92
+ lines = [line.rstrip() for line in rendered.replace("\r\n", "\n").split("\n")]
93
+ out = _BLANK_RUNS.sub("\n\n", "\n".join(lines)).strip("\n")
94
+ return out + "\n" if out else ""
95
+
96
+
97
+ def _template_set(resolved: ResolvedProfile, root: Path) -> TemplateSet:
98
+ overrides = {
99
+ name: str(path if (path := Path(value)).is_absolute() else root / path)
100
+ for name, value in sorted(resolved.profile.templates.override.items())
101
+ if name == TEMPLATE_NAME or name.startswith(BLOCK_PREFIX)
102
+ }
103
+ layers: list[TemplateLayer] = []
104
+ if overrides:
105
+ layers.append(TemplateLayer(name="project", override=overrides))
106
+ layers.append(TemplateLayer(name="pack", directory=_TEMPLATE_DIR))
107
+ return TemplateSet(layers)
108
+
109
+
110
+ def render_agents_md(resolved_profile: ResolvedProfile, root: Path) -> str:
111
+ """The managed block's content for the project at `root`, without the markers.
112
+
113
+ It starts with the one-line generated note, then one section per kind of profile data
114
+ present. The same profile always gives the same bytes. Raises `AgentsMdError` if the
115
+ naming rules cannot be read or a template override is broken.
116
+ """
117
+ context = build_context(resolved_profile, root)
118
+ context["code"] = code
119
+ context["text"] = text
120
+ try:
121
+ body = _template_set(resolved_profile, root).render(TEMPLATE_NAME, context)
122
+ except TemplateSetError as exc:
123
+ raise AgentsMdError(f"cannot render the AGENTS.md block: {exc}") from exc
124
+ block = _normalize(body)
125
+ if BEGIN_MARKER in block or END_MARKER in block:
126
+ raise AgentsMdError("the rendered AGENTS.md block must not contain the chipgraph markers")
127
+ return f"{NOTE}\n\n{block}"
128
+
129
+
130
+ def agents_md_content(existing: str | None, resolved_profile: ResolvedProfile, root: Path) -> str:
131
+ """The full file content: `existing` with its managed block (re)generated."""
132
+ return update_agents_md(existing, render_agents_md(resolved_profile, root))
133
+
134
+
135
+ __all__ = [
136
+ "BEGIN_MARKER",
137
+ "BLOCK_PREFIX",
138
+ "END_MARKER",
139
+ "NOTE",
140
+ "TEMPLATE_NAME",
141
+ "AgentsMdError",
142
+ "agents_md_content",
143
+ "code",
144
+ "render_agents_md",
145
+ "split_managed",
146
+ "text",
147
+ "update_agents_md",
148
+ ]
@@ -0,0 +1,239 @@
1
+ """Build the template context for the managed `AGENTS.md` block from a resolved profile.
2
+
3
+ Everything here is plain data (dicts, lists, strings, booleans), sorted where the profile
4
+ has no meaningful order, so the rendered block is deterministic. No path in the context
5
+ is made absolute: values are shown as the profile spells them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+ from pydantic import ValidationError
16
+
17
+ from chipgraph.checks._naming_rules import NamingRules
18
+ from chipgraph.core.config import ConfigError, ResolvedProfile, resolve_data_ref
19
+ from chipgraph.core.config.models import AdapterCfg, PathRule
20
+ from chipgraph.packs.spec_core.gen.agents_md._markers import AgentsMdError
21
+
22
+ _WS = re.compile(r"\s+")
23
+
24
+
25
+ def _one_line(value: object) -> str:
26
+ return _WS.sub(" ", str(value)).strip()
27
+
28
+
29
+ def _as_list(value: object) -> list[str]:
30
+ if value is None:
31
+ return []
32
+ if isinstance(value, str):
33
+ return [value]
34
+ if isinstance(value, list | tuple):
35
+ return [str(v) for v in value]
36
+ return [str(value)]
37
+
38
+
39
+ def _command(value: object) -> str | None:
40
+ if isinstance(value, str):
41
+ return _one_line(value)
42
+ if isinstance(value, list | tuple) and value:
43
+ return " ".join(str(v) for v in value)
44
+ return None
45
+
46
+
47
+ def _check_row(check_id: str, cfg: AdapterCfg) -> dict[str, Any]:
48
+ extras = cfg.model_dump()
49
+ severity = extras.get("severity")
50
+ return {
51
+ "id": check_id,
52
+ "use": cfg.use,
53
+ "severity": str(severity) if severity is not None else None,
54
+ "command": _command(extras.get("cmd")) if cfg.use == "cmd" else None,
55
+ }
56
+
57
+
58
+ def _path_row(glob: str, rule: PathRule, block: str | None) -> dict[str, Any]:
59
+ return {
60
+ "glob": glob,
61
+ "block": block,
62
+ "checks": [f"{name} {rule.checks[name]}" for name in sorted(rule.checks)],
63
+ "write": rule.write,
64
+ "reason": _one_line(rule.reason) if rule.reason else None,
65
+ }
66
+
67
+
68
+ def _load_naming(ref: str, root: Path) -> dict[str, Any]:
69
+ try:
70
+ path = resolve_data_ref(ref, root)
71
+ except ConfigError as exc:
72
+ raise AgentsMdError(f"naming rules {ref!r}: {exc}") from exc
73
+ try:
74
+ data = yaml.safe_load(path.read_bytes())
75
+ except (OSError, yaml.YAMLError) as exc:
76
+ raise AgentsMdError(f"naming rules {ref!r}: cannot read: {exc}") from exc
77
+ try:
78
+ rules = NamingRules.model_validate(data)
79
+ except ValidationError as exc:
80
+ first = exc.errors()[0]
81
+ loc = ".".join(str(p) for p in first["loc"])
82
+ raise AgentsMdError(f"naming rules {ref!r}: invalid: {loc}: {first['msg']}") from exc
83
+ vocabulary = None
84
+ if rules.vocabulary is not None:
85
+ vocabulary = {
86
+ "rule": rules.vocabulary.rule,
87
+ "pairs": [[k, rules.vocabulary.replace[k]] for k in sorted(rules.vocabulary.replace)],
88
+ }
89
+ return {
90
+ "ref": ref,
91
+ "document": rules.document,
92
+ "version": rules.version,
93
+ "ignore_comment": rules.ignore_comment,
94
+ # The rule file's own order (module, port, ...) is kept: it follows the source
95
+ # document, and it is as deterministic as the file itself.
96
+ "identifiers": [
97
+ {
98
+ "kind": kind,
99
+ "pattern": rule.pattern,
100
+ "rule": rule.rule,
101
+ "message": _one_line(rule.message),
102
+ }
103
+ for kind, rule in rules.identifiers.items()
104
+ ],
105
+ "lexical": [{"rule": lex.rule, "message": _one_line(lex.message)} for lex in rules.lexical],
106
+ "vocabulary": vocabulary,
107
+ }
108
+
109
+
110
+ def _naming_refs(resolved: ResolvedProfile) -> list[str]:
111
+ profile = resolved.profile
112
+ refs: list[str] = []
113
+ if profile.naming.rules:
114
+ refs.append(profile.naming.rules)
115
+ for check_id in sorted(profile.adapters):
116
+ cfg = profile.adapters[check_id]
117
+ rules = cfg.model_dump().get("rules")
118
+ if cfg.use == "naming" and isinstance(rules, str) and rules not in refs:
119
+ refs.append(rules)
120
+ return refs
121
+
122
+
123
+ def _generated(resolved: ResolvedProfile) -> list[str]:
124
+ profile = resolved.profile
125
+ globs: set[str] = set(_as_list(profile.layout.get("generated")))
126
+ for cfg in profile.adapters.values():
127
+ extras = cfg.model_dump()
128
+ if cfg.use == "generated":
129
+ globs.update(_as_list(extras.get("files")))
130
+ globs.update(_as_list(extras.get("generated")))
131
+ return sorted(globs)
132
+
133
+
134
+ def _requirements(resolved: ResolvedProfile) -> tuple[dict[str, Any], list[dict[str, Any]]]:
135
+ profile = resolved.profile
136
+ req = profile.spec.requirements
137
+ per_block: list[dict[str, Any]] = []
138
+ for name in sorted(profile.blocks):
139
+ if profile.blocks[name].spec.requirements is None:
140
+ continue
141
+ eff = resolved.for_block(name).spec.requirements
142
+ per_block.append(
143
+ {"block": name, "id_pattern": eff.id_pattern, "infer": eff.infer == "verification"}
144
+ )
145
+ uses_block = "{block}" in req.id_pattern or "{BLOCK}" in req.id_pattern
146
+ overridden = {row["block"] for row in per_block}
147
+ example = None
148
+ if uses_block:
149
+ candidates = [b for b in sorted(profile.blocks) if b not in overridden]
150
+ if candidates:
151
+ example = {"block": candidates[0], "pattern": req.id_regex(candidates[0]).pattern}
152
+ trace = [cid for cid in sorted(profile.adapters) if profile.adapters[cid].use == "trace"]
153
+ return (
154
+ {
155
+ "id_pattern": req.id_pattern,
156
+ "uses_block": uses_block,
157
+ "example": example,
158
+ "infer": req.infer == "verification",
159
+ "infer_heading": req.infer_heading,
160
+ "trace_check": trace[0] if trace else None,
161
+ },
162
+ per_block,
163
+ )
164
+
165
+
166
+ def build_context(resolved: ResolvedProfile, root: Path) -> dict[str, Any]:
167
+ """The data the `agents_md.md.j2` template renders, from `resolved` and `root`.
168
+
169
+ `root` is only used to resolve the naming rules reference (``org:``, ``preset:``,
170
+ ``path:`` or relative), exactly as the `naming` check does.
171
+ """
172
+ profile = resolved.profile
173
+
174
+ layout = [
175
+ {"kind": kind, "templates": _as_list(profile.layout[kind])}
176
+ for kind in sorted(profile.layout)
177
+ ]
178
+ block_layout = [
179
+ {"block": name, "kind": kind, "templates": _as_list(override.layout[kind])}
180
+ for name, override in sorted(profile.blocks.items())
181
+ for kind in sorted(override.layout)
182
+ ]
183
+ all_templates = [t for row in (*layout, *block_layout) for t in row["templates"]]
184
+
185
+ block_checks = [
186
+ {"block": name, **_check_row(check_id, override.adapters[check_id])}
187
+ for name, override in sorted(profile.blocks.items())
188
+ for check_id in sorted(override.adapters)
189
+ ]
190
+
191
+ paths = [_path_row(glob, profile.paths[glob], None) for glob in sorted(profile.paths)]
192
+ paths += [
193
+ _path_row(glob, override.paths[glob], name)
194
+ for name, override in sorted(profile.blocks.items())
195
+ for glob in sorted(override.paths)
196
+ ]
197
+
198
+ checker = profile.naming.checker
199
+ requirements, block_requirements = _requirements(resolved)
200
+ chip = profile.spec.chip
201
+
202
+ return {
203
+ "project": {
204
+ "name": profile.project,
205
+ "extends": list(profile.extends),
206
+ "packs": list(profile.packs),
207
+ "target_kind": profile.target.kind,
208
+ "pdk": profile.target.pdk,
209
+ "chip_spec": {"path": chip.path, "format": chip.format} if chip else None,
210
+ "ip_dir": profile.spec.ip_dir,
211
+ },
212
+ "blocks": [
213
+ {"name": name, "instances": list(profile.blocks[name].instances)}
214
+ for name in sorted(profile.blocks)
215
+ ],
216
+ "layout": layout,
217
+ "block_layout": block_layout,
218
+ "placeholders": [p for p in ("{block}", "{BLOCK}") if any(p in t for t in all_templates)],
219
+ "naming": [_load_naming(ref, root) for ref in _naming_refs(resolved)],
220
+ "naming_checker": (
221
+ {"use": checker.use, "command": _command(checker.model_dump().get("cmd"))}
222
+ if checker is not None
223
+ else None
224
+ ),
225
+ "checks": [_check_row(cid, profile.adapters[cid]) for cid in sorted(profile.adapters)],
226
+ "block_checks": block_checks,
227
+ "requirements": requirements,
228
+ "block_requirements": block_requirements,
229
+ "paths": paths,
230
+ "nda": (
231
+ {"paths": sorted(set(profile.data.nda_paths)), "model": profile.data.nda_model}
232
+ if profile.data.nda_paths
233
+ else None
234
+ ),
235
+ "generated": _generated(resolved),
236
+ }
237
+
238
+
239
+ __all__ = ["build_context"]
@@ -0,0 +1,131 @@
1
+ """The managed block of an `AGENTS.md`: find the markers, replace only what is between.
2
+
3
+ DESIGN.md 4.7 / DECISIONS D31: chipgraph owns only the text between
4
+ ``<!-- chipgraph:begin -->`` and ``<!-- chipgraph:end -->``; everything else in the file
5
+ belongs to the team and is kept byte for byte.
6
+
7
+ A marker is recognised only as a whole line (surrounding spaces, tabs and a trailing
8
+ ``\\r`` allowed), so a team's own prose that mentions the marker inline is not mistaken
9
+ for one. Malformed markers are an error, never a guess.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ BEGIN_MARKER = "<!-- chipgraph:begin -->"
15
+ END_MARKER = "<!-- chipgraph:end -->"
16
+
17
+
18
+ class AgentsMdError(Exception):
19
+ """The file's markers are malformed, or the managed block could not be rendered."""
20
+
21
+
22
+ def _marker_lines(text: str, marker: str) -> list[tuple[int, int, int]]:
23
+ """Every whole-line occurrence of `marker`: (line number, start offset, end offset).
24
+
25
+ The offsets span the marker line without its line ending.
26
+ """
27
+ found: list[tuple[int, int, int]] = []
28
+ offset = 0
29
+ for number, line in enumerate(text.splitlines(keepends=True), start=1):
30
+ body = line.rstrip("\r\n")
31
+ if body.strip(" \t") == marker:
32
+ found.append((number, offset, offset + len(body)))
33
+ offset += len(line)
34
+ return found
35
+
36
+
37
+ def split_managed(existing: str) -> tuple[str, str, str] | None:
38
+ """Split `existing` into (text up to and including the begin marker line's marker,
39
+ the managed text between the markers, text from the end marker on).
40
+
41
+ Returns `None` if the file has no markers at all. Raises `AgentsMdError` for a begin
42
+ marker without an end, an end without a begin, more than one of either, or an end
43
+ marker before the begin marker.
44
+ """
45
+ begins = _marker_lines(existing, BEGIN_MARKER)
46
+ ends = _marker_lines(existing, END_MARKER)
47
+ if not begins and not ends:
48
+ return None
49
+ if len(begins) > 1:
50
+ lines = ", ".join(str(n) for n, _, _ in begins)
51
+ raise AgentsMdError(
52
+ f"{BEGIN_MARKER} appears {len(begins)} times (lines {lines}); keep exactly one "
53
+ "managed block"
54
+ )
55
+ if len(ends) > 1:
56
+ lines = ", ".join(str(n) for n, _, _ in ends)
57
+ raise AgentsMdError(
58
+ f"{END_MARKER} appears {len(ends)} times (lines {lines}); keep exactly one "
59
+ "managed block"
60
+ )
61
+ if not ends:
62
+ raise AgentsMdError(
63
+ f"{BEGIN_MARKER} on line {begins[0][0]} has no matching {END_MARKER}; add the "
64
+ "end marker or remove the begin marker"
65
+ )
66
+ if not begins:
67
+ raise AgentsMdError(
68
+ f"{END_MARKER} on line {ends[0][0]} has no {BEGIN_MARKER} before it; add the "
69
+ "begin marker or remove the end marker"
70
+ )
71
+ begin_line, _, begin_end = begins[0]
72
+ end_line, end_start, _ = ends[0]
73
+ if end_line < begin_line:
74
+ raise AgentsMdError(
75
+ f"{END_MARKER} (line {end_line}) comes before {BEGIN_MARKER} (line "
76
+ f"{begin_line}); the begin marker must come first"
77
+ )
78
+ # The begin marker's own line ending stays with the managed text's replacement.
79
+ return existing[:begin_end], existing[begin_end:end_start], existing[end_start:]
80
+
81
+
82
+ def _check_block(block: str) -> None:
83
+ if _marker_lines(block, BEGIN_MARKER) or _marker_lines(block, END_MARKER):
84
+ raise AgentsMdError("the managed block must not contain the chipgraph markers itself")
85
+
86
+
87
+ def wrap_block(block: str) -> str:
88
+ """`block` between the two markers, newline-terminated."""
89
+ _check_block(block)
90
+ body = block if block.endswith("\n") or not block else block + "\n"
91
+ return f"{BEGIN_MARKER}\n{body}{END_MARKER}\n"
92
+
93
+
94
+ def update_agents_md(existing: str | None, block: str) -> str:
95
+ """The file content after putting `block` in the managed part of `existing`.
96
+
97
+ - `existing` is `None` (no file) or empty: the markers with `block` between them.
98
+ - `existing` has no markers: the team's content unchanged, one blank line, then the
99
+ marked block appended.
100
+ - `existing` has one well-formed pair: only the text between the markers is replaced;
101
+ every byte outside them (and the marker lines themselves) is kept.
102
+ - Malformed markers raise `AgentsMdError`.
103
+
104
+ `block` is the managed content without the markers (as `render_agents_md` returns).
105
+ Applying the same `block` twice gives the same result as applying it once.
106
+ """
107
+ _check_block(block)
108
+ if existing is None or existing == "":
109
+ return wrap_block(block)
110
+ parts = split_managed(existing)
111
+ if parts is None:
112
+ if existing.endswith("\n\n"):
113
+ sep = ""
114
+ elif existing.endswith("\n"):
115
+ sep = "\n"
116
+ else:
117
+ sep = "\n\n"
118
+ return existing + sep + wrap_block(block)
119
+ head, _, tail = parts
120
+ body = block if block.endswith("\n") or not block else block + "\n"
121
+ return f"{head}\n{body}{tail}"
122
+
123
+
124
+ __all__ = [
125
+ "BEGIN_MARKER",
126
+ "END_MARKER",
127
+ "AgentsMdError",
128
+ "split_managed",
129
+ "update_agents_md",
130
+ "wrap_block",
131
+ ]