@adia-ai/adia-ui-factory 0.2.4 → 0.8.0

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 (144) hide show
  1. package/.claude-plugin/plugin.json +5 -7
  2. package/.mcp.json +1 -1
  3. package/CHANGELOG.md +27 -67
  4. package/README.md +32 -53
  5. package/agents/app-architect.md +28 -0
  6. package/agents/routing-corpus.json +202 -0
  7. package/agents/screen-composer.md +27 -0
  8. package/bin/adia-lint +17 -2
  9. package/commands/adia-genui.md +5 -8
  10. package/commands/adia-migrate.md +5 -6
  11. package/commands/adia-orient.md +5 -10
  12. package/commands/adia-scaffold.md +7 -13
  13. package/commands/adia-verify.md +5 -6
  14. package/package.json +5 -6
  15. package/references/a2ui-mcp-tools.md +11 -8
  16. package/references/authoring-components.md +25 -1
  17. package/references/component-model.md +52 -4
  18. package/references/contracts/a2ui-mcp-surface.md +35 -0
  19. package/references/contracts/migration-guide-format.md +34 -0
  20. package/references/data-and-hydration.md +8 -5
  21. package/references/genui-a2ui.md +12 -6
  22. package/references/llm.md +22 -39
  23. package/references/migration.md +110 -36
  24. package/references/project-shapes.md +11 -13
  25. package/references/shell-admin.md +5 -2
  26. package/references/shell-chat.md +5 -3
  27. package/references/shell-editor.md +1 -1
  28. package/references/shell-embed.md +40 -26
  29. package/references/shell-simple.md +5 -2
  30. package/references/spa-architecture.md +28 -73
  31. package/references/ssr-integration.md +1 -1
  32. package/references/verification.md +8 -12
  33. package/skills/adia-compose/SKILL.md +52 -0
  34. package/skills/{adia-ui-kit → adia-compose}/assets/figma-make/guidelines/Guidelines.md +3 -3
  35. package/skills/{adia-ui-kit → adia-compose}/assets/figma-make/guidelines/components.md +1 -1
  36. package/skills/{adia-ui-kit → adia-compose}/assets/figma-make/guidelines/styles.md +1 -1
  37. package/skills/{adia-ui-kit → adia-compose}/assets/figma-make/guidelines/tokens.md +3 -3
  38. package/skills/{adia-ui-kit → adia-compose}/assets/templates/FEEDBACK.template.md +7 -7
  39. package/skills/{adia-ui-kit → adia-compose}/assets/templates/RESPONSE.template.md +6 -7
  40. package/skills/adia-compose/references/composition-traps.md +58 -0
  41. package/skills/adia-compose/references/feedback-discipline.md +20 -0
  42. package/skills/adia-compose/references/spec-to-ui-reasoning.md +158 -0
  43. package/skills/adia-data/SKILL.md +78 -0
  44. package/skills/adia-genui/SKILL.md +83 -0
  45. package/skills/adia-host/SKILL.md +119 -0
  46. package/skills/adia-llm/SKILL.md +36 -0
  47. package/skills/adia-migrate/SKILL.md +76 -0
  48. package/skills/adia-orient/SKILL.md +123 -0
  49. package/skills/adia-project/SKILL.md +83 -0
  50. package/skills/adia-shells/SKILL.md +74 -0
  51. package/skills/adia-verify/SKILL.md +43 -0
  52. package/skills/adia-verify/references/verification.md +42 -0
  53. package/commands/adia-compose.md +0 -10
  54. package/commands/adia-wire.md +0 -13
  55. package/skills/adia-ui-compose/SKILL.md +0 -57
  56. package/skills/adia-ui-data/SKILL.md +0 -62
  57. package/skills/adia-ui-factory/SKILL.md +0 -113
  58. package/skills/adia-ui-genui/SKILL.md +0 -74
  59. package/skills/adia-ui-kit/CHANGELOG.md +0 -41
  60. package/skills/adia-ui-kit/SKILL.md +0 -442
  61. package/skills/adia-ui-kit/assets/team-scaffold/archive-README.md +0 -30
  62. package/skills/adia-ui-kit/assets/team-scaffold/drafts-README.md +0 -57
  63. package/skills/adia-ui-kit/assets/team-scaffold/inbox-README.md +0 -76
  64. package/skills/adia-ui-kit/assets/team-scaffold/outbox-README.md +0 -46
  65. package/skills/adia-ui-kit/assets/team-scaffold/team-root.README.md +0 -51
  66. package/skills/adia-ui-kit/assets/team-scaffold/tickets-README.md +0 -208
  67. package/skills/adia-ui-kit/assets/team-scaffold/tickets-REGISTRY.md +0 -81
  68. package/skills/adia-ui-kit/assets/templates/AGENTS.template.md +0 -128
  69. package/skills/adia-ui-kit/assets/templates/CLAUDE.template.md +0 -1
  70. package/skills/adia-ui-kit/assets/templates/figma-make-guidelines.md +0 -90
  71. package/skills/adia-ui-kit/evals/adversarial-content-trust.json +0 -83
  72. package/skills/adia-ui-kit/evals/evals.json +0 -55
  73. package/skills/adia-ui-kit/evals/routing-corpus.json +0 -121
  74. package/skills/adia-ui-kit/evals/teach-routing-cases.json +0 -92
  75. package/skills/adia-ui-kit/references/capabilities.md +0 -245
  76. package/skills/adia-ui-kit/references/catalog-lookup.md +0 -186
  77. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v1.md +0 -8
  78. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v2.md +0 -3585
  79. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v3.md +0 -1899
  80. package/skills/adia-ui-kit/references/common-gotchas-consumer.md +0 -522
  81. package/skills/adia-ui-kit/references/component-literacy.md +0 -291
  82. package/skills/adia-ui-kit/references/composition.md +0 -245
  83. package/skills/adia-ui-kit/references/correction-loop.md +0 -462
  84. package/skills/adia-ui-kit/references/css-policy.md +0 -343
  85. package/skills/adia-ui-kit/references/decision-model.md +0 -158
  86. package/skills/adia-ui-kit/references/diagnostic-output.md +0 -57
  87. package/skills/adia-ui-kit/references/docs-catalog.md +0 -239
  88. package/skills/adia-ui-kit/references/exclusions.md +0 -225
  89. package/skills/adia-ui-kit/references/feedback-authoring.md +0 -466
  90. package/skills/adia-ui-kit/references/figma-make.md +0 -244
  91. package/skills/adia-ui-kit/references/flows-auth.md +0 -307
  92. package/skills/adia-ui-kit/references/flows-content.md +0 -206
  93. package/skills/adia-ui-kit/references/flows-data.md +0 -348
  94. package/skills/adia-ui-kit/references/flows-interaction.md +0 -181
  95. package/skills/adia-ui-kit/references/flows-shells.md +0 -336
  96. package/skills/adia-ui-kit/references/gap-detection.md +0 -266
  97. package/skills/adia-ui-kit/references/harness.md +0 -284
  98. package/skills/adia-ui-kit/references/loading-protocol.md +0 -406
  99. package/skills/adia-ui-kit/references/local-link.md +0 -198
  100. package/skills/adia-ui-kit/references/mcp.md +0 -401
  101. package/skills/adia-ui-kit/references/migration.md +0 -116
  102. package/skills/adia-ui-kit/references/mission.md +0 -194
  103. package/skills/adia-ui-kit/references/mode-routing.md +0 -298
  104. package/skills/adia-ui-kit/references/patterns-agent.md +0 -223
  105. package/skills/adia-ui-kit/references/patterns-data.md +0 -1065
  106. package/skills/adia-ui-kit/references/patterns-forms.md +0 -223
  107. package/skills/adia-ui-kit/references/patterns-layout.md +0 -326
  108. package/skills/adia-ui-kit/references/patterns-recipes.md +0 -1416
  109. package/skills/adia-ui-kit/references/patterns-shells.md +0 -1097
  110. package/skills/adia-ui-kit/references/patterns-traits.md +0 -259
  111. package/skills/adia-ui-kit/references/patterns-wiring.md +0 -339
  112. package/skills/adia-ui-kit/references/pev-rationale.md +0 -137
  113. package/skills/adia-ui-kit/references/reasoning-gate.md +0 -146
  114. package/skills/adia-ui-kit/references/reasoning-methodologies.md +0 -112
  115. package/skills/adia-ui-kit/references/recon.md +0 -192
  116. package/skills/adia-ui-kit/references/rendering-model.md +0 -431
  117. package/skills/adia-ui-kit/references/response-authoring.md +0 -398
  118. package/skills/adia-ui-kit/references/session-log-schema.md +0 -203
  119. package/skills/adia-ui-kit/references/session-log-template.md +0 -118
  120. package/skills/adia-ui-kit/references/setup.md +0 -652
  121. package/skills/adia-ui-kit/references/skill-hygiene.md +0 -135
  122. package/skills/adia-ui-kit/references/spec-to-ui-reasoning.md +0 -394
  123. package/skills/adia-ui-kit/references/state.md +0 -229
  124. package/skills/adia-ui-kit/references/teach-protocol.md +0 -421
  125. package/skills/adia-ui-kit/references/theme.md +0 -142
  126. package/skills/adia-ui-kit/references/version-check.md +0 -69
  127. package/skills/adia-ui-kit/scripts/audit-decision-trace.mjs +0 -198
  128. package/skills/adia-ui-kit/scripts/audit-kit-roster.mjs +0 -366
  129. package/skills/adia-ui-kit/scripts/changelog-archive.mjs +0 -126
  130. package/skills/adia-ui-kit/scripts/check-adia-versions.py +0 -404
  131. package/skills/adia-ui-kit/scripts/run-skill-evals.mjs +0 -358
  132. package/skills/adia-ui-kit/scripts/validate-session-log.mjs +0 -242
  133. package/skills/adia-ui-kit/skill.json +0 -102
  134. package/skills/adia-ui-llm/SKILL.md +0 -51
  135. package/skills/adia-ui-migrate/SKILL.md +0 -64
  136. package/skills/adia-ui-migration/CHANGELOG.md +0 -47
  137. package/skills/adia-ui-migration/SKILL.md +0 -381
  138. package/skills/adia-ui-migration/skill.json +0 -29
  139. package/skills/adia-ui-project/SKILL.md +0 -77
  140. package/skills/adia-ui-shells/SKILL.md +0 -62
  141. package/skills/adia-ui-spa/SKILL.md +0 -52
  142. package/skills/adia-ui-ssr/SKILL.md +0 -52
  143. package/skills/adia-ui-verify/SKILL.md +0 -44
  144. /package/skills/{adia-ui-kit → adia-compose}/assets/templates/FEEDBACK-OMISSIONS-DEFENSE.template.md +0 -0
@@ -1,404 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- check-adia-versions.py — verify your @adia-ai/* install is sane.
4
-
5
- Run this from any consumer project (not just the monorepo). Walks three
6
- sources and reports drift:
7
-
8
- 1. Installed (node_modules/@adia-ai/<pkg>/package.json)
9
- 2. Declared (your package.json `dependencies` + `devDependencies`)
10
- 3. Latest published (npm view @adia-ai/<pkg> version — needs network)
11
-
12
- Exits 0 when everything lines up. Exits 1 on drift. Exits 2 on
13
- unrecoverable error (missing node_modules, malformed package.json).
14
-
15
- Usage
16
- -----
17
- ./check-adia-versions.py # full check (network)
18
- ./check-adia-versions.py --offline # skip npm view, lockstep only
19
- ./check-adia-versions.py --json # machine-readable output
20
- ./check-adia-versions.py --root . # explicit consumer root
21
-
22
- Why this exists
23
- ---------------
24
- The 9 `@adia-ai/*` packages ship in lockstep — one stale install desyncs
25
- the whole bundle. Plus the skill documents fixes that only apply at
26
- specific versions (e.g. F-014/F-015: v0.6.7 ships broken absolute-path
27
- imports in `@adia-ai/web-modules`, fix lands in v0.6.8). Without a
28
- verification step, agents trust their installed snapshot blindly and
29
- ship advice that's wrong for the user's actual install.
30
-
31
- Output legend
32
- -------------
33
- ✓ ok installed == declared, and matches latest (or --offline)
34
- 🔗 linked declared as `file:...`, `link:...`, or `workspace:*` —
35
- local development link, not an npm version (informational)
36
- ⚠ stale installed < latest published — `npm update` recommended
37
- ⚠ split lockstep broken — packages disagree on version
38
- ⚠ undeclared installed in node_modules but missing from package.json
39
- ⚠ uninstalled declared but missing from node_modules — `npm install`
40
- ✗ ahead installed > latest published — local source build /
41
- pre-publish state (informational, not an error in this
42
- monorepo where you may dev against unpublished cuts)
43
- """
44
-
45
- from __future__ import annotations
46
-
47
- import argparse
48
- import json
49
- import re
50
- import subprocess
51
- import sys
52
- from dataclasses import dataclass, field
53
- from pathlib import Path
54
- from typing import Optional
55
-
56
-
57
- PACKAGE_NAMES = [
58
- "@adia-ai/web-components",
59
- "@adia-ai/web-modules",
60
- "@adia-ai/llm",
61
- "@adia-ai/a2ui-runtime",
62
- "@adia-ai/a2ui-compose",
63
- "@adia-ai/a2ui-corpus",
64
- "@adia-ai/a2ui-mcp",
65
- "@adia-ai/a2ui-retrieval",
66
- "@adia-ai/a2ui-validator",
67
- ]
68
-
69
-
70
- @dataclass
71
- class PackageStatus:
72
- name: str
73
- installed: Optional[str] = None
74
- declared: Optional[str] = None # raw range from package.json
75
- declared_normalized: Optional[str] = None # version stripped of ^~
76
- latest: Optional[str] = None
77
- symlinked: bool = False # node_modules/<name> is a symlink
78
- issues: list[str] = field(default_factory=list)
79
-
80
-
81
- def read_json(path: Path) -> Optional[dict]:
82
- try:
83
- return json.loads(path.read_text())
84
- except (FileNotFoundError, json.JSONDecodeError):
85
- return None
86
-
87
-
88
- def is_link_spec(spec: Optional[str]) -> bool:
89
- """Detect local-development link specs (file:, link:, workspace:)."""
90
- if not spec:
91
- return False
92
- return spec.startswith(("file:", "link:", "workspace:", "portal:"))
93
-
94
-
95
- def normalize_range(spec: str) -> str:
96
- """Strip leading ^ ~ >= etc. from a semver range to get a version."""
97
- if not spec:
98
- return ""
99
- # Link specs aren't semver — leave them alone for display
100
- if is_link_spec(spec):
101
- return spec
102
- m = re.match(r"^[\^~><=\s]*([\d]+\.[\d]+\.[\d]+(?:-[\w.]+)?)", spec)
103
- return m.group(1) if m else spec
104
-
105
-
106
- def parse_semver(v: Optional[str]) -> tuple[int, int, int]:
107
- if not v:
108
- return (0, 0, 0)
109
- m = re.match(r"^(\d+)\.(\d+)\.(\d+)", v)
110
- if not m:
111
- return (0, 0, 0)
112
- return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
113
-
114
-
115
- def get_installed(root: Path, name: str) -> Optional[str]:
116
- pkg = root / "node_modules" / name / "package.json"
117
- data = read_json(pkg)
118
- return data.get("version") if data else None
119
-
120
-
121
- def is_symlinked(root: Path, name: str) -> bool:
122
- """Check whether node_modules/<name> is a symlink (linked) vs a real dir."""
123
- pkg_dir = root / "node_modules" / name
124
- try:
125
- return pkg_dir.is_symlink()
126
- except OSError:
127
- return False
128
-
129
-
130
- def get_declared(consumer_pkg: dict, name: str) -> Optional[str]:
131
- for field_name in ("dependencies", "devDependencies", "peerDependencies"):
132
- spec = (consumer_pkg.get(field_name) or {}).get(name)
133
- if spec:
134
- return spec
135
- return None
136
-
137
-
138
- def get_latest(name: str, timeout: int = 10) -> Optional[str]:
139
- """Query npm registry for the dist-tag latest version. Network call."""
140
- try:
141
- result = subprocess.run(
142
- ["npm", "view", name, "version"],
143
- capture_output=True, text=True, timeout=timeout,
144
- )
145
- if result.returncode == 0:
146
- return result.stdout.strip() or None
147
- except (subprocess.TimeoutExpired, FileNotFoundError):
148
- pass
149
- return None
150
-
151
-
152
- def check_lockstep(statuses: list[PackageStatus]) -> Optional[str]:
153
- """Return the consensus installed version, or None if split."""
154
- versions = {s.installed for s in statuses if s.installed}
155
- if len(versions) == 0:
156
- return None
157
- if len(versions) == 1:
158
- return versions.pop()
159
- return None # split
160
-
161
-
162
- def run_audit(root: Path, offline: bool) -> tuple[list[PackageStatus], dict]:
163
- consumer_pkg = read_json(root / "package.json") or {}
164
-
165
- # Monorepo-root detection: if this package.json declares "workspaces",
166
- # we're inside the @adia-ai/* source monorepo, not a consumer project.
167
- # In that case, "undeclared" is expected (the packages are workspaces,
168
- # not external deps) — suppress the noise.
169
- is_monorepo_root = bool(consumer_pkg.get("workspaces"))
170
-
171
- # "undeclared" classification — only flag when it likely reflects a
172
- # mistake. In a consumer project, sub-packages like @adia-ai/llm,
173
- # a2ui-runtime, a2ui-mcp are commonly transitive deps pulled in by
174
- # @adia-ai/web-components or web-modules; "undeclared" there is noise.
175
- # We only flag undeclared when:
176
- # (a) we are NOT in the monorepo root (workspaces detected), AND
177
- # (b) the package is one of the user-facing entry points
178
- # (web-components, web-modules) AND it's installed.
179
- # This catches the real failure mode (someone deleted the dep but
180
- # node_modules still has it = phantom install) without spamming on
181
- # legitimate transitive deps.
182
- USER_FACING = {"@adia-ai/web-components", "@adia-ai/web-modules"}
183
-
184
- statuses: list[PackageStatus] = []
185
- for name in PACKAGE_NAMES:
186
- s = PackageStatus(name=name)
187
- s.installed = get_installed(root, name)
188
- s.declared = get_declared(consumer_pkg, name)
189
- s.declared_normalized = normalize_range(s.declared) if s.declared else None
190
- s.symlinked = is_symlinked(root, name)
191
- if not offline:
192
- s.latest = get_latest(name)
193
-
194
- declared_is_link = is_link_spec(s.declared)
195
-
196
- # Per-package classification
197
- if s.declared and not s.installed:
198
- s.issues.append("uninstalled")
199
- if (s.installed and not s.declared
200
- and not is_monorepo_root
201
- and name in USER_FACING):
202
- s.issues.append("undeclared")
203
- # Link-spec packages get a "linked" marker; suppress version-drift
204
- # warnings since semver comparison doesn't apply to file:/link:/workspace:.
205
- if declared_is_link:
206
- s.issues.append("linked")
207
- # Pitfall warning: declared as link but installed as real dir
208
- # (npm sometimes copies instead of linking)
209
- if s.installed and not s.symlinked:
210
- s.issues.append("link-copied-not-symlinked")
211
- elif s.installed and s.latest:
212
- inst = parse_semver(s.installed)
213
- late = parse_semver(s.latest)
214
- if inst < late:
215
- s.issues.append("stale")
216
- elif inst > late:
217
- s.issues.append("ahead")
218
-
219
- statuses.append(s)
220
-
221
- consensus = check_lockstep(statuses)
222
- summary = {
223
- "consumer_root": str(root.resolve()),
224
- "consensus_installed": consensus,
225
- "split_lockstep": consensus is None and any(s.installed for s in statuses),
226
- "offline": offline,
227
- "is_monorepo_root": is_monorepo_root,
228
- }
229
- return statuses, summary
230
-
231
-
232
- def render_text(statuses: list[PackageStatus], summary: dict) -> int:
233
- """Human-readable output. Returns process exit code."""
234
- print(f"@adia-ai install audit — {summary['consumer_root']}")
235
- if summary.get("is_monorepo_root"):
236
- print("(monorepo-root mode — workspaces detected, 'undeclared' suppressed)")
237
- print("─" * 78)
238
-
239
- any_issues = False
240
- consensus_latest = None
241
- for s in statuses:
242
- if s.latest:
243
- consensus_latest = s.latest
244
- break # first one with a latest is enough for the header line
245
-
246
- if consensus_latest:
247
- print(f"Latest published (npm view): {consensus_latest}")
248
- if summary["consensus_installed"]:
249
- print(f"Consensus installed: {summary['consensus_installed']}")
250
- elif summary["split_lockstep"]:
251
- print("Consensus installed: ⚠ SPLIT — packages disagree")
252
- any_issues = True
253
- print()
254
-
255
- # Per-package table
256
- name_width = max(len(s.name) for s in statuses) + 2
257
- print(f" {'package'.ljust(name_width)} {'installed':<10} {'declared':<26} {'latest':<10} status")
258
- print(f" {'-' * name_width} {'-' * 10} {'-' * 26} {'-' * 10} {'-' * 24}")
259
-
260
- for s in statuses:
261
- installed = s.installed or "—"
262
- declared = s.declared or "—"
263
- # Truncate long file: specs for table readability
264
- if declared and len(declared) > 26:
265
- declared = declared[:23] + "..."
266
- latest = s.latest or ("(offline)" if summary["offline"] else "—")
267
-
268
- if not s.issues:
269
- status = "✓ ok"
270
- else:
271
- status_parts = []
272
- for i in s.issues:
273
- if i == "linked":
274
- status_parts.append("🔗 linked")
275
- elif i == "ahead":
276
- status_parts.append("ⓘ ahead")
277
- elif i == "link-copied-not-symlinked":
278
- status_parts.append("⚠ copied-not-linked")
279
- else:
280
- status_parts.append(f"⚠ {i}")
281
- status = " · ".join(status_parts)
282
- # Only count as a real failure if it's not purely informational
283
- real_issues = [i for i in s.issues
284
- if i not in ("ahead", "linked")]
285
- if real_issues:
286
- any_issues = True
287
-
288
- print(f" {s.name.ljust(name_width)} {installed:<10} {declared:<26} {latest:<10} {status}")
289
-
290
- print()
291
-
292
- # Diagnostics
293
- if summary["split_lockstep"]:
294
- print("✗ Lockstep broken — the 9 @adia-ai/* packages must share a version.")
295
- print(" Fix: `npm install @adia-ai/web-components@<v> @adia-ai/web-modules@<v> ...`")
296
- print(" or delete node_modules + package-lock.json and `npm install`.")
297
- print()
298
-
299
- stale_pkgs = [s for s in statuses if "stale" in s.issues]
300
- if stale_pkgs:
301
- print(f"⚠ Stale: {len(stale_pkgs)} package(s) behind latest. Suggested:")
302
- print(f" npm install {' '.join(f'{s.name}@latest' for s in stale_pkgs)}")
303
- print()
304
-
305
- ahead_pkgs = [s for s in statuses if "ahead" in s.issues]
306
- if ahead_pkgs:
307
- print(f"ⓘ Ahead: {len(ahead_pkgs)} package(s) installed past latest published.")
308
- print(" This is normal in the monorepo (pre-publish dev cut) or if you")
309
- print(" installed from a local tarball / git tag. Not an error.")
310
- print()
311
-
312
- linked_pkgs = [s for s in statuses if "linked" in s.issues]
313
- if linked_pkgs:
314
- all_linked = len(linked_pkgs) == sum(1 for s in statuses if s.declared)
315
- print(f"🔗 Linked: {len(linked_pkgs)} package(s) declared as local "
316
- f"dev link ({'all declared' if all_linked else 'subset'}).")
317
- print(" Version comparison disabled for these — they resolve to source")
318
- print(" on disk, not an npm-published version. The 'installed' column")
319
- print(" shows the version from the linked source's package.json.")
320
- if not all_linked and sum(1 for s in statuses if s.declared) > len(linked_pkgs):
321
- print()
322
- print(" ⚠ Mixed link + npm declarations risk split-lockstep when the")
323
- print(" linked source's transitive deps disagree with what npm")
324
- print(" resolved. Recommended: link ALL 9 @adia-ai/* packages, or")
325
- print(" link NONE. See §LocalLink in the adia-ui-kit skill.")
326
- print()
327
-
328
- copied_pkgs = [s for s in statuses if "link-copied-not-symlinked" in s.issues]
329
- if copied_pkgs:
330
- print(f"⚠ Link copied, not symlinked: {len(copied_pkgs)} package(s).")
331
- print(" npm sometimes copies file: deps instead of symlinking — edits")
332
- print(" to source won't be reflected. Fix:")
333
- print(" rm -rf node_modules/@adia-ai")
334
- print(" npm install --install-links=false")
335
- print(" Then re-run this script to confirm symlinks are in place.")
336
- print()
337
-
338
- uninstalled = [s for s in statuses if "uninstalled" in s.issues]
339
- if uninstalled:
340
- print(f"⚠ Declared but uninstalled: {len(uninstalled)} package(s).")
341
- print(" Fix: `npm install`")
342
- print()
343
-
344
- if not any_issues:
345
- print("✓ All clean — lockstep aligned, declarations match installs, "
346
- "and (if online) installs match latest published.")
347
- return 0
348
-
349
- return 1
350
-
351
-
352
- def render_json(statuses: list[PackageStatus], summary: dict) -> int:
353
- payload = {
354
- "summary": summary,
355
- "packages": [
356
- {
357
- "name": s.name,
358
- "installed": s.installed,
359
- "declared": s.declared,
360
- "declared_normalized": s.declared_normalized,
361
- "latest": s.latest,
362
- "symlinked": s.symlinked,
363
- "issues": s.issues,
364
- }
365
- for s in statuses
366
- ],
367
- }
368
- print(json.dumps(payload, indent=2))
369
- # 'ahead' and 'linked' are informational, not failures
370
- real_issues = summary["split_lockstep"] or any(
371
- i for s in statuses for i in s.issues if i not in ("ahead", "linked")
372
- )
373
- return 1 if real_issues else 0
374
-
375
-
376
- def main(argv: Optional[list[str]] = None) -> int:
377
- parser = argparse.ArgumentParser(
378
- description="Verify @adia-ai/* install lockstep + freshness.",
379
- )
380
- parser.add_argument("--root", default=".",
381
- help="Consumer project root (default: cwd).")
382
- parser.add_argument("--offline", action="store_true",
383
- help="Skip npm view (no network).")
384
- parser.add_argument("--json", action="store_true",
385
- help="Machine-readable JSON output.")
386
- args = parser.parse_args(argv)
387
-
388
- root = Path(args.root).resolve()
389
- if not (root / "package.json").exists():
390
- print(f"✗ No package.json at {root}", file=sys.stderr)
391
- return 2
392
- if not (root / "node_modules").exists():
393
- print(f"⚠ No node_modules at {root} — run `npm install` first", file=sys.stderr)
394
- return 2
395
-
396
- statuses, summary = run_audit(root, args.offline)
397
-
398
- if args.json:
399
- return render_json(statuses, summary)
400
- return render_text(statuses, summary)
401
-
402
-
403
- if __name__ == "__main__":
404
- sys.exit(main())