@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15

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 (253) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +102 -52
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/capability-stewardship.md +1 -1
  10. package/docs/blocks/claims.md +2 -2
  11. package/docs/blocks/conformance-admission.md +1 -1
  12. package/docs/blocks/learning-harnesses.md +1 -1
  13. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  14. package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
  15. package/docs/guides/co-authoring.md +113 -0
  16. package/docs/{distributions.md → guides/distributions.md} +13 -8
  17. package/docs/guides/extensions.md +189 -0
  18. package/docs/guides/graph-refresh.md +152 -0
  19. package/docs/guides/graph-workspace.md +64 -0
  20. package/docs/{ingestion.md → guides/ingestion.md} +16 -15
  21. package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
  22. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  23. package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
  24. package/docs/{learning.md → guides/learning.md} +12 -5
  25. package/docs/{obsidian.md → guides/obsidian.md} +370 -23
  26. package/docs/guides/ontology-guide.md +111 -0
  27. package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
  28. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  29. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  30. package/docs/{upgrade.md → guides/upgrade.md} +197 -11
  31. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  32. package/docs/knowledge-health/facilitator.md +15 -4
  33. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  34. package/docs/knowledge-health/quickstart.md +97 -75
  35. package/docs/knowledge-health/readme.md +13 -5
  36. package/docs/maintainers/describing-the-atelier.md +173 -0
  37. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
  38. package/docs/{announcements.md → reference/announcements.md} +1 -1
  39. package/docs/{architecture.md → reference/architecture.md} +31 -5
  40. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  41. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  42. package/docs/{coordination.md → reference/coordination.md} +1 -1
  43. package/docs/{decisions.md → reference/decisions.md} +3 -3
  44. package/docs/{design.md → reference/design.md} +1 -1
  45. package/docs/reference/edge-policy.md +151 -0
  46. package/docs/reference/graph-view-structured.md +119 -0
  47. package/docs/reference/graph-view.md +139 -0
  48. package/docs/reference/graph-workspace.md +37 -0
  49. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  50. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  51. package/docs/{local-services.md → reference/local-services.md} +43 -4
  52. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
  53. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
  54. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  55. package/docs/reference/portability.md +92 -0
  56. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  57. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  58. package/docs/{project-options.md → reference/project-options.md} +17 -0
  59. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
  60. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  61. package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
  62. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  63. package/docs/{trackables.md → reference/trackables.md} +2 -2
  64. package/docs/start/independent-start.md +89 -0
  65. package/docs/{install.md → start/install.md} +9 -9
  66. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  67. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  68. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  69. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  70. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  71. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  72. package/fixtures/knowledge-health/workshop/README.md +1 -1
  73. package/fixtures/templates/local-library.v1.json +3 -1
  74. package/package.json +22 -8
  75. package/plugins/obsidian/main.js +57 -8
  76. package/plugins/obsidian/manifest.json +1 -1
  77. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  78. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  79. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  80. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  81. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  82. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  83. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  84. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  85. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  86. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  87. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  88. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  89. package/src/architecture/catalog.mjs +2 -2
  90. package/src/attestation/sign.mjs +1 -1
  91. package/src/boundary/content-rules.mjs +534 -68
  92. package/src/boundary/policy.mjs +187 -60
  93. package/src/cli/run.mjs +26 -9
  94. package/src/commands/distribution.mjs +1 -1
  95. package/src/commands/extension.mjs +27 -0
  96. package/src/commands/ingest.mjs +1 -1
  97. package/src/commands/learn.mjs +1 -1
  98. package/src/commands/obsidian.mjs +49 -21
  99. package/src/commands/review.mjs +3 -0
  100. package/src/edge-policy/contract.mjs +69 -0
  101. package/src/edge-policy/policy.mjs +142 -0
  102. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  103. package/src/extension-packs/loader.mjs +4 -3
  104. package/src/extensions/admission.mjs +83 -0
  105. package/src/extensions/client.d.mts +23 -0
  106. package/src/extensions/client.mjs +99 -0
  107. package/src/extensions/contracts.d.mts +29 -0
  108. package/src/extensions/contracts.mjs +163 -0
  109. package/src/extensions/convex.d.mts +24 -0
  110. package/src/extensions/convex.mjs +189 -0
  111. package/src/extensions/delegation.mjs +54 -0
  112. package/src/extensions/drizzle.d.mts +11 -0
  113. package/src/extensions/drizzle.mjs +33 -0
  114. package/src/extensions/postgres.d.mts +7 -0
  115. package/src/extensions/postgres.mjs +66 -0
  116. package/src/extensions/prisma.d.mts +13 -0
  117. package/src/extensions/prisma.mjs +63 -0
  118. package/src/extensions/project.mjs +432 -0
  119. package/src/extensions/scaffold-convex.mjs +130 -0
  120. package/src/extensions/scaffold-sql.mjs +144 -0
  121. package/src/extensions/server.d.mts +20 -0
  122. package/src/extensions/server.mjs +102 -0
  123. package/src/extensions/sql-scope.mjs +22 -0
  124. package/src/graph/graph.mjs +13 -5
  125. package/src/graph/knowledge-graph.mjs +20 -2
  126. package/src/graph/repo-contribution.mjs +405 -0
  127. package/src/graph/repo-contribution.types.mjs +32 -0
  128. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  129. package/src/graph-access/edge-record-adapter.mjs +73 -0
  130. package/src/graph-access/index.mjs +253 -0
  131. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  132. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  133. package/src/graph-access/source-adapter.mjs +81 -0
  134. package/src/graph-refresh/compare.mjs +110 -0
  135. package/src/graph-refresh/controller.mjs +324 -0
  136. package/src/graph-refresh/index.mjs +3 -0
  137. package/src/graph-refresh/local.mjs +78 -0
  138. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  139. package/src/graph-view/babylon/index.mjs +2 -0
  140. package/src/graph-view/babylon/navigator.mjs +312 -0
  141. package/src/graph-view/babylon/provenance.mjs +36 -0
  142. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  143. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  144. package/src/graph-view/babylon/web-host.mjs +188 -0
  145. package/src/graph-view/cli.mjs +3 -0
  146. package/src/graph-view/command.mjs +34 -0
  147. package/src/graph-view/contracts.mjs +79 -0
  148. package/src/graph-view/errors.mjs +5 -0
  149. package/src/graph-view/index.mjs +4 -0
  150. package/src/graph-view/intents.mjs +30 -0
  151. package/src/graph-view/query.mjs +213 -0
  152. package/src/graph-view/refs.mjs +2 -0
  153. package/src/graph-view/structured/browser.mjs +262 -0
  154. package/src/graph-view/structured/styles.mjs +8 -0
  155. package/src/graph-workspace/authoring.mjs +185 -0
  156. package/src/graph-workspace/browser/constellation.mjs +369 -0
  157. package/src/graph-workspace/browser/dom.mjs +23 -0
  158. package/src/graph-workspace/browser/index.mjs +6 -0
  159. package/src/graph-workspace/browser/review.mjs +55 -0
  160. package/src/graph-workspace/browser/styles.mjs +11 -0
  161. package/src/graph-workspace/browser/workbench.mjs +351 -0
  162. package/src/graph-workspace/browser/workspace.mjs +431 -0
  163. package/src/graph-workspace/disclosure.mjs +39 -0
  164. package/src/graph-workspace/geometry.mjs +78 -0
  165. package/src/graph-workspace/index.mjs +11 -0
  166. package/src/graph-workspace/labels.mjs +50 -0
  167. package/src/graph-workspace/model.mjs +109 -0
  168. package/src/graph-workspace/recipes.mjs +88 -0
  169. package/src/graph-workspace/scale.mjs +95 -0
  170. package/src/graph-workspace/theme.mjs +156 -0
  171. package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
  172. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  173. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  174. package/src/knowledge/ontology-guide.mjs +152 -0
  175. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  176. package/src/knowledge-health/rule-identity.mjs +14 -9
  177. package/src/knowledge-health/workshop.mjs +22 -2
  178. package/src/personal-workspace/index.mjs +1 -1
  179. package/src/personal-workspace/selection.mjs +34 -3
  180. package/src/project/config.mjs +1 -1
  181. package/src/project/path-match.mjs +2 -1
  182. package/src/project/providers/github.mjs +24 -0
  183. package/src/project/repo-identity.mjs +3 -22
  184. package/src/projection/obsidian/contracts.mjs +1 -1
  185. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  186. package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
  187. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  188. package/src/projection/obsidian/personal-workspace.mjs +84 -5
  189. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  190. package/src/projection/obsidian/proposals/adapter.mjs +46 -23
  191. package/src/projection/obsidian/proposals/observation.mjs +40 -2
  192. package/src/projection/obsidian/publication/publisher.mjs +96 -8
  193. package/src/projection/project.mjs +21 -16
  194. package/src/readiness/readiness.mjs +20 -11
  195. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  196. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  197. package/src/readiness-protocols/evidence.mjs +2 -1
  198. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  199. package/src/readiness-protocols/runtime.mjs +43 -6
  200. package/src/runtime/obsidian/engine.mjs +387 -76
  201. package/src/runtime/obsidian/lifecycle.mjs +20 -15
  202. package/src/runtime/obsidian/login-item.mjs +6 -3
  203. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  204. package/src/runtime/obsidian/observation.mjs +36 -4
  205. package/src/runtime/obsidian/opening.mjs +4 -3
  206. package/src/runtime/obsidian/personal-selection.mjs +187 -0
  207. package/src/runtime/obsidian/pipeline.mjs +115 -6
  208. package/src/runtime/obsidian/private-lock.mjs +50 -6
  209. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  210. package/src/runtime/obsidian/service-main.mjs +68 -16
  211. package/src/runtime/obsidian/service.mjs +80 -6
  212. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  213. package/src/runtime/obsidian/view-permission.mjs +335 -0
  214. package/src/ui/control-system.mjs +201 -0
  215. package/src/ui/html-primitives.mjs +3 -0
  216. package/src/upgrade/closure-diagnostics.mjs +135 -0
  217. package/src/upgrade/closure.mjs +649 -0
  218. package/src/upgrade/upgrade.mjs +42 -11
  219. package/templates/distribution-workspace/README.md +2 -2
  220. package/templates/external-project-workspace/atelier.project.json +3 -0
  221. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  222. package/templates/knowledge-workspace/README.md +2 -2
  223. package/docs/integration-and-review-plan.md +0 -409
  224. package/docs/integration-contract-decisions.md +0 -44
  225. package/docs/knowledge-health/devday/facilitator.md +0 -16
  226. package/docs/knowledge-health/devday/public-default.md +0 -107
  227. package/docs/knowledge-health/devday/quickstart.md +0 -18
  228. package/docs/knowledge-health/devday/support.md +0 -22
  229. package/docs/knowledge-health/exercise.md +0 -17
  230. package/docs/knowledge-health/support.md +0 -34
  231. package/docs/presentation-review-disposition.md +0 -47
  232. package/docs/progressive-ingestion-acceptance.md +0 -89
  233. package/docs/progressive-ingestion.md +0 -235
  234. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  235. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  236. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  237. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  238. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  239. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  240. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  241. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  242. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  243. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  244. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  245. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  246. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  247. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  248. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  249. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  250. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  251. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  252. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  253. /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
@@ -0,0 +1,201 @@
1
+ const CONTROL_VARIANTS = new Set(['primary', 'secondary', 'quiet'])
2
+ const CONTROL_SIZES = new Set(['default', 'compact'])
3
+ const GROUP_LAYOUTS = new Set(['inline', 'grid'])
4
+
5
+ function enumValue(value, allowed, fallback) {
6
+ return allowed.has(value) ? value : fallback
7
+ }
8
+
9
+ function htmlEscape(value) {
10
+ return String(value ?? '')
11
+ .replaceAll('&', '&')
12
+ .replaceAll('<', '&lt;')
13
+ .replaceAll('>', '&gt;')
14
+ .replaceAll('"', '&quot;')
15
+ .replaceAll("'", '&#39;')
16
+ }
17
+
18
+ function htmlAttr(name, value) {
19
+ if (value == null || value === false) return ''
20
+ if (value === true) return ` ${name}`
21
+ return ` ${name}="${htmlEscape(value)}"`
22
+ }
23
+
24
+ function htmlAttrs(attrs = {}) {
25
+ return Object.entries(attrs).map(([name, value]) => htmlAttr(name, value)).join('')
26
+ }
27
+
28
+ function controlAttrs(attrs = {}) {
29
+ return Object.fromEntries(Object.entries(attrs).filter(([name]) => (
30
+ /^(?:id|name|value|title|aria-[a-z0-9-]+|data-[a-z0-9-]+)$/.test(name)
31
+ )))
32
+ }
33
+
34
+ /**
35
+ * Tenant-neutral control geometry and interaction behavior shared by Atelier
36
+ * tooling and compatible publication surfaces. Consumers theme the primitive
37
+ * exclusively through the --system-control-* custom properties below.
38
+ * Generated markup carries no upstream project or package identity.
39
+ */
40
+ export const atelierControlStyles = `
41
+ .system-control {
42
+ display: inline-flex;
43
+ min-height: var(--system-control-height, 44px);
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: var(--system-control-gap, 0.5rem);
47
+ padding: 0 var(--system-control-pad-inline, 1.5rem);
48
+ border: 0;
49
+ border-radius: var(--system-control-radius, 0.875rem);
50
+ font: inherit;
51
+ font-size: var(--system-control-font-size, 1rem);
52
+ font-weight: var(--system-control-font-weight, 650);
53
+ line-height: 1.1;
54
+ letter-spacing: 0;
55
+ white-space: nowrap;
56
+ text-decoration: none;
57
+ cursor: pointer;
58
+ user-select: none;
59
+ transition:
60
+ background-color 0.2s var(--system-control-ease, ease),
61
+ color 0.2s var(--system-control-ease, ease),
62
+ box-shadow 0.2s var(--system-control-ease, ease),
63
+ transform 0.25s var(--system-control-ease, ease);
64
+ }
65
+
66
+ .system-control[data-variant="primary"] {
67
+ background: var(--system-control-primary-background, #c89d50);
68
+ color: var(--system-control-primary-foreground, #17110a);
69
+ }
70
+
71
+ .system-control[data-variant="secondary"] {
72
+ background: var(--system-control-secondary-background, transparent);
73
+ color: var(--system-control-secondary-foreground, #c89d50);
74
+ box-shadow: inset 0 0 0 1px var(--system-control-secondary-border, currentColor);
75
+ }
76
+
77
+ .system-control[data-variant="quiet"] {
78
+ background: var(--system-control-quiet-background, transparent);
79
+ color: var(--system-control-quiet-foreground, #c89d50);
80
+ }
81
+
82
+ .system-control[data-size="compact"] {
83
+ padding-inline: var(--system-control-compact-pad-inline, 1.125rem);
84
+ border-radius: var(--system-control-compact-radius, 0.6875rem);
85
+ font-size: var(--system-control-compact-font-size, 0.9375rem);
86
+ }
87
+
88
+ @media (hover: hover) and (pointer: fine) {
89
+ .system-control[data-variant="primary"]:hover {
90
+ background: var(--system-control-primary-hover-background, #b88d43);
91
+ color: var(--system-control-primary-hover-foreground, #17110a);
92
+ box-shadow: inset 0 0 0 1px var(--system-control-primary-hover-border, rgba(255, 255, 255, 0.25));
93
+ }
94
+
95
+ .system-control[data-variant="secondary"]:hover {
96
+ background: var(--system-control-secondary-hover-background, transparent);
97
+ color: var(--system-control-secondary-hover-foreground, #f4eee5);
98
+ box-shadow: inset 0 0 0 1px var(--system-control-secondary-hover-border, currentColor);
99
+ }
100
+
101
+ .system-control[data-variant="quiet"]:hover {
102
+ background: var(--system-control-quiet-hover-background, rgba(255, 255, 255, 0.02));
103
+ color: var(--system-control-quiet-hover-foreground, #f4eee5);
104
+ }
105
+ }
106
+
107
+ .system-control:active {
108
+ transform: translateY(1px);
109
+ transition-duration: 0.06s;
110
+ }
111
+
112
+ .system-control:focus-visible {
113
+ outline: none;
114
+ box-shadow: 0 0 0 3px var(--system-control-focus, rgba(200, 157, 80, 0.3));
115
+ }
116
+
117
+ .system-control:disabled,
118
+ .system-control[aria-disabled="true"] {
119
+ cursor: not-allowed;
120
+ opacity: 0.55;
121
+ }
122
+
123
+ .system-action-group {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ gap: var(--system-action-group-gap, 0.75rem);
127
+ margin: 0;
128
+ padding: 0;
129
+ list-style: none;
130
+ }
131
+
132
+ .system-action-group[data-layout="grid"] {
133
+ display: grid;
134
+ grid-template-columns: repeat(2, minmax(0, 1fr));
135
+ }
136
+
137
+ .system-action-group[data-layout="grid"] .system-control {
138
+ width: 100%;
139
+ justify-content: space-between;
140
+ }
141
+
142
+ @media (max-width: 40rem) {
143
+ .system-action-group[data-layout="grid"] {
144
+ grid-template-columns: 1fr;
145
+ }
146
+ }
147
+
148
+ @media (prefers-reduced-motion: reduce) {
149
+ .system-control,
150
+ .system-control:active {
151
+ transform: none;
152
+ transition: none;
153
+ }
154
+ }
155
+ `.trim()
156
+
157
+ export function renderControl({
158
+ label,
159
+ href,
160
+ type = 'button',
161
+ variant = 'secondary',
162
+ size = 'default',
163
+ external = false,
164
+ arrow = false,
165
+ attrs = {},
166
+ } = {}) {
167
+ const resolvedVariant = enumValue(variant, CONTROL_VARIANTS, 'secondary')
168
+ const resolvedSize = enumValue(size, CONTROL_SIZES, 'default')
169
+ const content = `${htmlEscape(label)}${arrow ? '<span aria-hidden="true">→</span>' : ''}`
170
+ const shared = {
171
+ class: 'system-control',
172
+ 'data-variant': resolvedVariant,
173
+ 'data-size': resolvedSize,
174
+ ...controlAttrs(attrs),
175
+ }
176
+
177
+ if (typeof href === 'string' && href.length > 0) {
178
+ return `<a${htmlAttr('href', href)}${htmlAttrs({
179
+ ...shared,
180
+ ...(external ? { target: '_blank', rel: 'noreferrer' } : {}),
181
+ })}>${content}</a>`
182
+ }
183
+
184
+ return `<button${htmlAttrs({ type, ...shared })}>${content}</button>`
185
+ }
186
+
187
+ export function renderActionGroup(actions = [], {
188
+ label = 'Actions',
189
+ layout = 'inline',
190
+ variant = 'secondary',
191
+ size = 'default',
192
+ } = {}) {
193
+ const resolvedLayout = enumValue(layout, GROUP_LAYOUTS, 'inline')
194
+ const items = actions.map((action) => `<li>${renderControl({
195
+ ...action,
196
+ variant: action.variant ?? variant,
197
+ size: action.size ?? size,
198
+ })}</li>`).join('')
199
+
200
+ return `<nav aria-label="${htmlEscape(label)}"><ul class="system-action-group" data-layout="${resolvedLayout}">${items}</ul></nav>`
201
+ }
@@ -1,3 +1,5 @@
1
+ import { atelierControlStyles } from './control-system.mjs'
2
+
1
3
  export function htmlEscape(value) {
2
4
  return String(value ?? '')
3
5
  .replaceAll('&', '&amp;')
@@ -35,6 +37,7 @@ export function renderPageShell({ title = 'MNSTRY Atelier', body = '' } = {}) {
35
37
  '.meta{color:#666;font-size:13px}',
36
38
  'pre{white-space:pre-wrap;background:#1f2424;color:#f5f5ee;border-radius:8px;padding:16px;overflow:auto}',
37
39
  '.notice{border-left:4px solid #245f73;padding:12px 16px;background:#edf6f7}',
40
+ atelierControlStyles,
38
41
  '</style>',
39
42
  '</head>',
40
43
  '<body>',
@@ -0,0 +1,135 @@
1
+ // Pure classifiers for the two consumer-install failures that PR #89 showed
2
+ // with no typed diagnostic. Nothing here runs npm or touches the network:
3
+ // consumer-smoke.mjs passes in npm's stderr and lockfiles and turns a finding
4
+ // into an AtelierDiagnosticError.
5
+ //
6
+ // - consumer-closure-incomplete: an offline install asked for a package the
7
+ // warmed cache does not hold (npm's ENOTCACHED).
8
+ // - override-not-inherited: a consumer's own lockfile holds a version, or a
9
+ // second copy, of a package the publisher pins with `overrides`. npm applies
10
+ // overrides only in the root project, so a consumer never receives them.
11
+
12
+ export const CONSUMER_CLOSURE_INCOMPLETE = 'consumer-closure-incomplete'
13
+ export const OVERRIDE_NOT_INHERITED = 'override-not-inherited'
14
+
15
+ const ENOTCACHED = /^npm (?:error|ERR!) code ENOTCACHED$/m
16
+ const REQUEST_URL = /^npm (?:error|ERR!) request to (\S+) failed/m
17
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
18
+ // An override key may select versions (`name@range`); only a bare name is an
19
+ // unconditional pin.
20
+ const SELECTOR_KEY = /^(?:@[^/@]+\/)?[^/@]+@/
21
+ // npm colours its output on a terminal and Windows hosts end lines with CRLF.
22
+ const ANSI = /\u001b\[[0-9;]*m/g
23
+
24
+ // The package and version npm asked for, from a registry URL. A tarball URL
25
+ // (`<registry>/<name>/-/<basename>-<version>.tgz`) names both; a packument URL
26
+ // (`<registry>/<name>`) names only the package.
27
+ function requestedPackage(url) {
28
+ let pathname
29
+ try { pathname = decodeURIComponent(new URL(url).pathname) } catch { return { package: null, version: null } }
30
+ const tarball = pathname.match(/^\/((?:@[^/]+\/)?[^/]+)\/-\/([^/]+)\.tgz$/)
31
+ if (tarball) {
32
+ const [, name, file] = tarball
33
+ const prefix = `${name.split('/').pop()}-`
34
+ return { package: name, version: file.startsWith(prefix) ? file.slice(prefix.length) : null }
35
+ }
36
+ const packument = pathname.match(/^\/((?:@[^/]+\/)?[^/]+)\/?$/)
37
+ return { package: packument ? packument[1] : null, version: null }
38
+ }
39
+
40
+ // A finding for npm stderr that reports ENOTCACHED, else null. Any other npm
41
+ // failure is not classified here and keeps its own error.
42
+ export function classifyNpmFailure(stderr) {
43
+ const text = String(stderr ?? '').replace(ANSI, '').replace(/\r\n?/g, '\n')
44
+ if (!ENOTCACHED.test(text)) return null
45
+ const url = text.match(REQUEST_URL)?.[1] ?? null
46
+ return { code: CONSUMER_CLOSURE_INCOMPLETE, ...(url ? requestedPackage(url) : { package: null, version: null }), url }
47
+ }
48
+
49
+ const isEntry = (entry) => entry !== null && typeof entry === 'object' && !Array.isArray(entry)
50
+ const underNodeModules = (path) => /(?:^|\/)node_modules\//.test(path)
51
+
52
+ // Every installed copy of `name` in a v2/v3 lockfile, one per node_modules
53
+ // path, sorted by path. A copy's name is the entry's own `name` (set for an
54
+ // npm: alias) or else the path after its last node_modules/. The root entry
55
+ // ('') is the consumer itself, and links are not copies. A path outside
56
+ // node_modules is a folder of the project (the target of a link): it is never
57
+ // a copy, and no name is derived from it. An entry that is not an object is
58
+ // not a copy either.
59
+ function installedCopies(lockfile, name) {
60
+ return Object.entries(lockfile?.packages ?? {})
61
+ .filter(([path, entry]) => path !== '' && isEntry(entry) && !entry.link && underNodeModules(path)
62
+ && (entry.name ?? path.slice(path.lastIndexOf('node_modules/') + 'node_modules/'.length)) === name)
63
+ .map(([path, entry]) => ({ path, version: entry.version ?? null }))
64
+ .sort((left, right) => left.path.localeCompare(right.path))
65
+ }
66
+
67
+ // One finding per publisher override the consumer's lockfile does not honour:
68
+ // a copy at another version, or more than one copy. An override whose package
69
+ // the consumer does not install has nothing to honour; `compared` names the
70
+ // overrides that had at least one copy to check. Only unconditional
71
+ // exact-version overrides can be compared, so a version-selector key or a
72
+ // non-exact value is refused rather than skipped.
73
+ export function overrideFindings({ publisherOverrides, consumerLock }) {
74
+ if (!consumerLock || typeof consumerLock.packages !== 'object' || consumerLock.packages === null) {
75
+ throw new TypeError('consumer lockfile must be lockfileVersion 2 or 3 with a packages map')
76
+ }
77
+ const findings = []
78
+ const compared = []
79
+ for (const [name, pinned] of Object.entries(publisherOverrides ?? {})) {
80
+ if (SELECTOR_KEY.test(name)) {
81
+ throw new TypeError(`override key ${name} selects versions; the captured-closure check compares unconditional pins only`)
82
+ }
83
+ if (typeof pinned !== 'string' || !EXACT_VERSION.test(pinned)) {
84
+ throw new TypeError(`override for ${name} is not an exact version; the captured-closure check compares exact pins only`)
85
+ }
86
+ const found = installedCopies(consumerLock, name)
87
+ if (found.length > 0) compared.push(name)
88
+ if (found.length > 1 || found.some((copy) => copy.version !== pinned)) {
89
+ findings.push({ code: OVERRIDE_NOT_INHERITED, package: name, pinned, found })
90
+ }
91
+ }
92
+ return { findings, compared }
93
+ }
94
+
95
+ // The registry tarballs an offline `npm ci` needs from the consumer's own
96
+ // lockfile: every entry with an http(s) `resolved` URL and an `integrity`.
97
+ // Local entries (the candidate's `file:` tarball, links) and bundled entries
98
+ // are not cached.
99
+ // `missing` lists registry-installed paths that lack either field, which an
100
+ // offline reinstall could not satisfy. An entry that is not an object is
101
+ // listed as missing too: it names nothing that could be reinstalled.
102
+ export function capturedClosure(consumerLock) {
103
+ const tarballs = []
104
+ const missing = []
105
+ for (const [path, entry] of Object.entries(consumerLock?.packages ?? {})) {
106
+ if (path === '') continue
107
+ if (!isEntry(entry)) {
108
+ missing.push(path)
109
+ continue
110
+ }
111
+ if (entry.link || entry.inBundle) continue
112
+ const resolved = typeof entry.resolved === 'string' ? entry.resolved : null
113
+ if (resolved && !/^https?:\/\//.test(resolved)) continue
114
+ if (resolved && typeof entry.integrity === 'string') tarballs.push({ path, resolved, integrity: entry.integrity })
115
+ else missing.push(path)
116
+ }
117
+ const byPath = (left, right) => (typeof left === 'string' ? left.localeCompare(right) : left.path.localeCompare(right.path))
118
+ return { tarballs: tarballs.sort(byPath), missing: missing.sort(byPath) }
119
+ }
120
+
121
+ // The installed tree from `npm ls --all --json`, one sorted line per package
122
+ // with its ancestry, so two installs of the same project can be compared and
123
+ // the first difference named.
124
+ export function npmTreeLines(npmLsJson) {
125
+ const lines = []
126
+ const visit = (dependencies, ancestry) => {
127
+ for (const [name, entry] of Object.entries(dependencies ?? {})) {
128
+ const line = `${ancestry}${name}@${entry?.version ?? 'missing'}`
129
+ lines.push(line)
130
+ visit(entry?.dependencies, `${line} > `)
131
+ }
132
+ }
133
+ visit(npmLsJson?.dependencies, '')
134
+ return lines.sort()
135
+ }