@geraldmaron/construct 1.1.0 → 1.2.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 (425) hide show
  1. package/README.md +30 -18
  2. package/bin/construct +392 -91
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +77 -10
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +232 -11
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +703 -0
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +302 -0
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +4 -2
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/doctor/watchers/service-health.mjs +1 -1
  124. package/lib/document-export.mjs +360 -36
  125. package/lib/document-extract.mjs +88 -0
  126. package/lib/document-ingest.mjs +32 -18
  127. package/lib/embed/cli.mjs +2 -2
  128. package/lib/embed/daemon.mjs +18 -46
  129. package/lib/embed/semantic.mjs +5 -2
  130. package/lib/embed/worker.mjs +2 -2
  131. package/lib/embedded-contract/model-resolve.mjs +10 -9
  132. package/lib/env-config.mjs +26 -0
  133. package/lib/evals/dataset.mjs +137 -0
  134. package/lib/evals/gates.mjs +175 -0
  135. package/lib/graph/build-co-change.mjs +54 -0
  136. package/lib/graph/build-from-registry.mjs +136 -0
  137. package/lib/graph/build-import-graph.mjs +153 -0
  138. package/lib/graph/cli.mjs +110 -0
  139. package/lib/graph/impact-cli.mjs +89 -0
  140. package/lib/graph/impact.mjs +108 -0
  141. package/lib/graph/staleness.mjs +44 -0
  142. package/lib/graph/store.mjs +172 -0
  143. package/lib/handoffs/cleanup.mjs +1 -1
  144. package/lib/health-check.mjs +90 -76
  145. package/lib/hooks/artifact-release-gate.mjs +43 -0
  146. package/lib/hooks/audit-trail.mjs +14 -28
  147. package/lib/hooks/brand-prose-lint.mjs +38 -0
  148. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  149. package/lib/hooks/session-optimize.mjs +84 -207
  150. package/lib/hooks/session-start.mjs +4 -5
  151. package/lib/host-disposition.mjs +7 -0
  152. package/lib/improvement/cli.mjs +189 -0
  153. package/lib/improvement/controller.mjs +137 -0
  154. package/lib/improvement/proposal.mjs +120 -0
  155. package/lib/improvement/specialist-loop.mjs +192 -0
  156. package/lib/improvement/store.mjs +89 -0
  157. package/lib/improvement/surface.mjs +219 -0
  158. package/lib/ingest-tooling.mjs +97 -0
  159. package/lib/init/doc-lanes.mjs +165 -0
  160. package/lib/init-docs.mjs +31 -178
  161. package/lib/init-unified.mjs +27 -97
  162. package/lib/init-update-guide.mjs +102 -0
  163. package/lib/init-update.mjs +32 -1
  164. package/lib/init.mjs +8 -0
  165. package/lib/install/desktop-binary-download.mjs +85 -0
  166. package/lib/install/legacy-global-cleanup.mjs +189 -0
  167. package/lib/intake/daemon.mjs +2 -0
  168. package/lib/intake/git-queue.mjs +9 -8
  169. package/lib/intake/queue.mjs +2 -1
  170. package/lib/intake/session-prelude.mjs +90 -1
  171. package/lib/libreoffice-export.mjs +97 -0
  172. package/lib/logging/rotate.mjs +9 -0
  173. package/lib/maintenance/docker-reclaim.mjs +206 -0
  174. package/lib/mcp/external-schema-cost.mjs +74 -0
  175. package/lib/mcp/server.mjs +84 -9
  176. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  177. package/lib/mcp/tool-budget.mjs +55 -4
  178. package/lib/mcp/tools/document.mjs +18 -4
  179. package/lib/mcp/tools/skills.mjs +32 -3
  180. package/lib/mcp/tools/workflow.mjs +13 -1
  181. package/lib/model-free-selector.mjs +18 -0
  182. package/lib/model-registry.mjs +13 -229
  183. package/lib/model-router.mjs +425 -93
  184. package/lib/models/behavior-matrix.mjs +289 -0
  185. package/lib/models/catalog.mjs +209 -0
  186. package/lib/models/execution-capability-profile.mjs +196 -0
  187. package/lib/models/execution-policy.mjs +307 -0
  188. package/lib/models/provider-poll.mjs +383 -0
  189. package/lib/npm-spawn-env.mjs +17 -0
  190. package/lib/ollama/installed-models.mjs +129 -0
  191. package/lib/oracle/actions.mjs +187 -0
  192. package/lib/oracle/artifact-gate.mjs +99 -0
  193. package/lib/oracle/cli.mjs +204 -0
  194. package/lib/oracle/daemon-entry.mjs +14 -0
  195. package/lib/oracle/dispatch.mjs +81 -0
  196. package/lib/oracle/execute.mjs +143 -0
  197. package/lib/oracle/gaps.mjs +76 -0
  198. package/lib/oracle/index.mjs +84 -0
  199. package/lib/oracle/issues.mjs +164 -0
  200. package/lib/oracle/org-graph.mjs +170 -0
  201. package/lib/oracle/policy.mjs +80 -0
  202. package/lib/oracle/read-model.mjs +398 -0
  203. package/lib/oracle/reconcile.mjs +191 -0
  204. package/lib/oracle/routing.mjs +89 -0
  205. package/lib/oracle/synthesize.mjs +384 -0
  206. package/lib/oracle/verdicts.mjs +51 -0
  207. package/lib/orchestration/worker.mjs +88 -27
  208. package/lib/orchestration-policy.mjs +50 -0
  209. package/lib/parity.mjs +96 -2
  210. package/lib/persona-sections.mjs +66 -0
  211. package/lib/playwright-demo.mjs +252 -0
  212. package/lib/project-init-shared.mjs +4 -1
  213. package/lib/prompt-composer.js +9 -4
  214. package/lib/prompt-validation-contract.mjs +24 -0
  215. package/lib/provider-capabilities.js +57 -11
  216. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  217. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  218. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  219. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  220. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  221. package/lib/providers/contract/contract-tests.mjs +57 -0
  222. package/lib/providers/contract/errors.mjs +48 -0
  223. package/lib/providers/contract/interface.mjs +50 -0
  224. package/lib/providers/contract/registry.mjs +102 -0
  225. package/lib/providers/copilot-auth.mjs +297 -0
  226. package/lib/providers/credential-bootstrap.mjs +178 -0
  227. package/lib/providers/credential-catalog.mjs +46 -0
  228. package/lib/providers/credential-sources.mjs +63 -0
  229. package/lib/providers/creds.mjs +5 -2
  230. package/lib/providers/op-run.mjs +59 -0
  231. package/lib/providers/secret-resolver.mjs +159 -0
  232. package/lib/publish-template.mjs +163 -0
  233. package/lib/publish-tooling.mjs +119 -0
  234. package/lib/publish.mjs +305 -0
  235. package/lib/registry/cli.mjs +82 -0
  236. package/lib/registry/consolidation.mjs +147 -0
  237. package/lib/registry/generate-docs.mjs +75 -0
  238. package/lib/registry/skill-verification.mjs +57 -0
  239. package/lib/registry/surface-map.mjs +76 -0
  240. package/lib/registry/validate.mjs +135 -0
  241. package/lib/resources/budget.mjs +82 -0
  242. package/lib/resources/process-budget.mjs +45 -0
  243. package/lib/rules-delivery.mjs +9 -2
  244. package/lib/rules-read.mjs +26 -0
  245. package/lib/runtime-env.mjs +23 -0
  246. package/lib/runtime-pressure.mjs +51 -7
  247. package/lib/schema-infer.mjs +13 -2
  248. package/lib/server/chat-loop.mjs +622 -0
  249. package/lib/server/demo-preview.mjs +63 -0
  250. package/lib/server/index.mjs +229 -8
  251. package/lib/server/langfuse-login.mjs +3 -3
  252. package/lib/service-manager.mjs +61 -9
  253. package/lib/session-store.mjs +1 -1
  254. package/lib/setup.mjs +102 -2
  255. package/lib/specialists/prompt-schema.mjs +19 -10
  256. package/lib/specialists/roster.mjs +43 -0
  257. package/lib/specialists/scaffold.mjs +56 -0
  258. package/lib/storage/backend.mjs +6 -6
  259. package/lib/storage/embeddings-local.mjs +6 -3
  260. package/lib/storage/file-lock.mjs +18 -11
  261. package/lib/storage/hybrid-query.mjs +7 -4
  262. package/lib/storage/state-source.mjs +5 -5
  263. package/lib/storage/sync.mjs +2 -3
  264. package/lib/telemetry/rule-calls.mjs +52 -0
  265. package/lib/template-registry.mjs +2 -2
  266. package/lib/templates/visual-requirements.mjs +27 -51
  267. package/lib/test-corpus-inventory.mjs +313 -0
  268. package/lib/uninstall/uninstall.mjs +19 -7
  269. package/lib/update.mjs +12 -0
  270. package/lib/upgrade.mjs +14 -0
  271. package/lib/wireframe.mjs +20 -14
  272. package/lib/worker/run.mjs +17 -4
  273. package/lib/worker/trace.mjs +11 -3
  274. package/package.json +29 -13
  275. package/personas/construct.md +13 -13
  276. package/platforms/claude/settings.template.json +36 -0
  277. package/rules/common/patterns.md +1 -1
  278. package/rules/common/release-gates.md +4 -3
  279. package/scripts/sync-specialists.mjs +187 -60
  280. package/skills/devops/data-engineering.md +1 -1
  281. package/skills/docs/adr-workflow.md +1 -0
  282. package/skills/docs/backlog-proposal-workflow.md +1 -0
  283. package/skills/docs/codebase-research-workflow.md +40 -0
  284. package/skills/docs/customer-profile-workflow.md +1 -0
  285. package/skills/docs/document-ingest-workflow.md +1 -0
  286. package/skills/docs/evidence-ingest-workflow.md +1 -0
  287. package/skills/docs/init-docs.md +2 -2
  288. package/skills/docs/init-project.md +8 -3
  289. package/skills/docs/prd-workflow.md +24 -1
  290. package/skills/docs/prfaq-workflow.md +1 -0
  291. package/skills/docs/product-intelligence-workflow.md +1 -0
  292. package/skills/docs/product-signal-workflow.md +1 -0
  293. package/skills/docs/research-workflow.md +54 -37
  294. package/skills/docs/runbook-workflow.md +1 -0
  295. package/skills/docs/strategy-workflow.md +1 -0
  296. package/skills/docs/user-research-workflow.md +40 -0
  297. package/skills/operating/orchestration-reference.md +1 -1
  298. package/skills/roles/architect.md +5 -0
  299. package/skills/roles/operator.docs.md +4 -0
  300. package/skills/routing.md +4 -2
  301. package/specialists/artifact-manifest.json +480 -0
  302. package/specialists/artifact-manifest.schema.json +53 -0
  303. package/specialists/audit-enrichments.json +454 -0
  304. package/specialists/contracts.json +37 -25
  305. package/specialists/prompts/_shared/validation-contract.md +26 -0
  306. package/specialists/prompts/cx-accessibility.md +21 -2
  307. package/specialists/prompts/cx-ai-engineer.md +24 -1
  308. package/specialists/prompts/cx-architect.md +4 -1
  309. package/specialists/prompts/cx-business-strategist.md +23 -2
  310. package/specialists/prompts/cx-data-analyst.md +22 -1
  311. package/specialists/prompts/cx-data-engineer.md +23 -2
  312. package/specialists/prompts/cx-debugger.md +21 -2
  313. package/specialists/prompts/cx-designer.md +26 -3
  314. package/specialists/prompts/cx-devil-advocate.md +19 -2
  315. package/specialists/prompts/cx-docs-keeper.md +28 -1
  316. package/specialists/prompts/cx-engineer.md +22 -1
  317. package/specialists/prompts/cx-evaluator.md +18 -1
  318. package/specialists/prompts/cx-explorer.md +21 -2
  319. package/specialists/prompts/cx-legal-compliance.md +21 -2
  320. package/specialists/prompts/cx-operations.md +21 -2
  321. package/specialists/prompts/cx-oracle.md +94 -0
  322. package/specialists/prompts/cx-orchestrator.md +20 -1
  323. package/specialists/prompts/cx-platform-engineer.md +25 -2
  324. package/specialists/prompts/cx-product-manager.md +32 -1
  325. package/specialists/prompts/cx-qa.md +24 -2
  326. package/specialists/prompts/cx-rd-lead.md +23 -2
  327. package/specialists/prompts/cx-release-manager.md +23 -2
  328. package/specialists/prompts/cx-researcher.md +22 -2
  329. package/specialists/prompts/cx-reviewer.md +18 -1
  330. package/specialists/prompts/cx-security.md +22 -2
  331. package/specialists/prompts/cx-sre.md +30 -3
  332. package/specialists/prompts/cx-test-automation.md +7 -1
  333. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  334. package/specialists/prompts/cx-ux-researcher.md +21 -2
  335. package/specialists/registry.json +52 -173
  336. package/specialists/tone-profiles.json +42 -0
  337. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  338. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  339. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  340. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  341. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  342. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  343. package/templates/demos/vhs/construct-cockpit.json +24 -0
  344. package/templates/distribution/construct-brand.typ +446 -0
  345. package/templates/distribution/construct-decision.typ +38 -0
  346. package/templates/distribution/construct-deck.html +95 -0
  347. package/templates/distribution/construct-pdf.typ +38 -0
  348. package/templates/distribution/construct-prd.typ +38 -0
  349. package/templates/distribution/construct-research.typ +38 -0
  350. package/templates/distribution/construct-web.html +92 -0
  351. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  352. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  353. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  354. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  355. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  356. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  357. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  358. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  359. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  360. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  361. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  362. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  363. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  364. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  365. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  366. package/templates/distribution/fonts/README.md +36 -0
  367. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  368. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  369. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  370. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  371. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  372. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  373. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  374. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  375. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  376. package/templates/distribution/icons/activity.svg +15 -0
  377. package/templates/distribution/icons/alert-triangle.svg +17 -0
  378. package/templates/distribution/icons/book-open.svg +16 -0
  379. package/templates/distribution/icons/bot.svg +20 -0
  380. package/templates/distribution/icons/brain.svg +22 -0
  381. package/templates/distribution/icons/circle-check.svg +16 -0
  382. package/templates/distribution/icons/clipboard-check.svg +17 -0
  383. package/templates/distribution/icons/cpu.svg +28 -0
  384. package/templates/distribution/icons/database.svg +17 -0
  385. package/templates/distribution/icons/eye.svg +16 -0
  386. package/templates/distribution/icons/file-text.svg +19 -0
  387. package/templates/distribution/icons/gauge.svg +16 -0
  388. package/templates/distribution/icons/git-branch.svg +17 -0
  389. package/templates/distribution/icons/key.svg +17 -0
  390. package/templates/distribution/icons/layers.svg +17 -0
  391. package/templates/distribution/icons/list-checks.svg +19 -0
  392. package/templates/distribution/icons/lock.svg +16 -0
  393. package/templates/distribution/icons/message-square.svg +15 -0
  394. package/templates/distribution/icons/network.svg +19 -0
  395. package/templates/distribution/icons/route.svg +17 -0
  396. package/templates/distribution/icons/scale.svg +19 -0
  397. package/templates/distribution/icons/search.svg +16 -0
  398. package/templates/distribution/icons/send.svg +16 -0
  399. package/templates/distribution/icons/server.svg +18 -0
  400. package/templates/distribution/icons/shield-check.svg +16 -0
  401. package/templates/distribution/icons/users.svg +18 -0
  402. package/templates/distribution/icons/webhook.svg +17 -0
  403. package/templates/distribution/icons/workflow.svg +17 -0
  404. package/templates/distribution/icons/wrench.svg +15 -0
  405. package/templates/distribution/run.mjs +18 -1
  406. package/templates/docs/adr.md +7 -0
  407. package/templates/docs/construct_guide.md +10 -10
  408. package/templates/docs/customer-profile.md +4 -0
  409. package/templates/docs/one-pager.md +4 -0
  410. package/templates/docs/postmortem.md +31 -0
  411. package/templates/docs/prd-platform.md +19 -0
  412. package/templates/docs/prd.md +15 -0
  413. package/templates/docs/prfaq.md +7 -0
  414. package/templates/docs/qa-strategy.md +103 -0
  415. package/templates/docs/research-brief.md +8 -0
  416. package/templates/docs/rfc-platform.md +12 -0
  417. package/templates/docs/test-plan.md +7 -0
  418. package/lib/ingest/chunker.mjs +0 -94
  419. package/lib/ingest/pipeline.mjs +0 -53
  420. package/lib/ingest/store.mjs +0 -82
  421. package/lib/mode-commands.mjs +0 -122
  422. package/lib/policy/unified-gates.mjs +0 -96
  423. package/lib/profiles/validate-custom.mjs +0 -114
  424. package/lib/services/telemetry-backend.mjs +0 -177
  425. package/lib/storage/fusion.mjs +0 -95
@@ -37,6 +37,10 @@ function daysAgo(value) {
37
37
  return (Date.now() - ts) / (24 * 60 * 60 * 1000);
38
38
  }
39
39
 
40
+ function beadUpdatedAt(bead) {
41
+ return bead?.updated || bead?.updatedAt || bead?.updated_at || bead?.created || bead?.createdAt || bead?.created_at || null;
42
+ }
43
+
40
44
  function listBeadsJson({ status, runner = spawnSync } = {}) {
41
45
  const args = ['list', '--json'];
42
46
  if (status) args.push(`--status=${status}`);
@@ -78,6 +82,20 @@ function titleAppearsInSubject(beadTitle, commitSubject) {
78
82
  return hits >= 3;
79
83
  }
80
84
 
85
+ /**
86
+ * Oracle auto-raised meta beads must not feed back into hygiene drift counts.
87
+ */
88
+ export function isOracleMetaBead(bead) {
89
+ const title = String(bead?.title ?? '');
90
+ const labels = Array.isArray(bead?.labels) ? bead.labels : [];
91
+ return title.startsWith('[oracle/') || labels.includes('oracle');
92
+ }
93
+
94
+ function filterOracleMeta(beads, excludeOracleMeta) {
95
+ if (!excludeOracleMeta) return beads;
96
+ return beads.filter((b) => !isOracleMetaBead(b));
97
+ }
98
+
81
99
  /**
82
100
  * Run the three drift checks against the live tracker + git state.
83
101
  * Pure-ish — accepts a runner injection for tests.
@@ -86,14 +104,17 @@ export function detectBeadsDrift({
86
104
  staleOpenDays = DEFAULT_STALE_OPEN_DAYS,
87
105
  stuckInProgressDays = DEFAULT_STUCK_IN_PROGRESS_DAYS,
88
106
  mergeLookback = DEFAULT_MERGE_LOOKBACK,
107
+ excludeOracleMeta = true,
89
108
  runner = spawnSync,
90
109
  } = {}) {
91
- const open = listBeadsJson({ status: 'open', runner });
92
- const inProgress = listBeadsJson({ status: 'in_progress', runner });
110
+ const openRaw = listBeadsJson({ status: 'open', runner });
111
+ const inProgressRaw = listBeadsJson({ status: 'in_progress', runner });
112
+ const open = filterOracleMeta(openRaw, excludeOracleMeta);
113
+ const inProgress = filterOracleMeta(inProgressRaw, excludeOracleMeta);
93
114
  const commits = recentCommitSubjects(mergeLookback, { runner });
94
115
 
95
- const staleOpen = open.filter((b) => daysAgo(b.updated || b.updatedAt) > staleOpenDays);
96
- const stuckInProgress = inProgress.filter((b) => daysAgo(b.updated || b.updatedAt) > stuckInProgressDays);
116
+ const staleOpen = open.filter((b) => daysAgo(beadUpdatedAt(b)) > staleOpenDays);
117
+ const stuckInProgress = inProgress.filter((b) => daysAgo(beadUpdatedAt(b)) > stuckInProgressDays);
97
118
 
98
119
  const mergeDrift = [];
99
120
  for (const bead of open) {
@@ -106,8 +127,8 @@ export function detectBeadsDrift({
106
127
  }
107
128
 
108
129
  return {
109
- staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
110
- stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
130
+ staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(beadUpdatedAt(b))) })),
131
+ stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(beadUpdatedAt(b))) })),
111
132
  mergeDrift,
112
133
  thresholds: { staleOpenDays, stuckInProgressDays, mergeLookback },
113
134
  counts: {
@@ -1,15 +1,9 @@
1
1
  /**
2
2
  * lib/beads-client.mjs — Wrapper for bd commands with lock‑management and queueing.
3
3
  *
4
- * VERSION 2.0: Now uses optimistic locking by default for better concurrency.
5
- * All external bd calls should go through `runBd()` to ensure serialized access
6
- * to the embedded Dolt database.
7
- *
8
- * Changes:
9
- * - Reads are now lock-free (concurrent safe)
10
- * - Writes use optimistic locking with automatic retry
11
- * - Legacy file-based lock is fallback only
12
- * - Better error messages and conflict resolution
4
+ * VERSION 2.0: optimistic locking by default; the exclusive file-lock fallback
5
+ * was retired after zero telemetry firings (construct-nhn5). Writes retry via
6
+ * Dolt versioning; reads are lock-free.
13
7
  */
14
8
 
15
9
  import { spawnSync } from 'node:child_process';
@@ -18,14 +12,10 @@ import path from 'node:path';
18
12
  import { fileURLToPath } from 'node:url';
19
13
 
20
14
  import {
21
- acquireLock,
22
- releaseLock,
23
15
  getLockStatus,
24
16
  cleanupStaleLock,
25
- enqueueRequest,
26
- removeFromQueue,
27
- readQueue,
28
17
  cleanupStaleQueue,
18
+ readQueue,
29
19
  } from './beads-lock.mjs';
30
20
 
31
21
  // Anchored bead-ID format. IDs reaching the bd shell boundary must match this;
@@ -67,7 +57,7 @@ const DEFAULT_OPTIONS = {
67
57
  queuePollMs: 100, // How often to check for lock release when queued
68
58
  maxQueueWaitSeconds: 300, // Max total time allowed in queue
69
59
  useOptimisticLocking: true, // NEW: Use optimistic locking by default
70
- fallbackToLegacy: true, // NEW: Fall back to legacy locking on failure
60
+ fallbackToLegacy: false,
71
61
  maxRetries: 5, // NEW: Max retries for optimistic locking
72
62
  };
73
63
 
@@ -237,122 +227,50 @@ export async function runBd(args, options = {}) {
237
227
 
238
228
  if (!opts.silent) {
239
229
  console.error(`[beads] ⚠ Optimistic locking failed: ${result.error}`);
240
- console.error(`[beads] ↘ Falling back to legacy lock...`);
241
230
  }
242
- }
243
-
244
- // ==========================================================================
245
- // PATH 3: Legacy file-based locking (fallback, most conservative)
246
- // ==========================================================================
247
- return runWithLegacyLock(args, opts, cwd, commandDesc);
248
- }
249
-
250
- /**
251
- * Legacy locking implementation (original behavior).
252
- * Used as fallback when optimistic locking fails.
253
- */
254
- async function runWithLegacyLock(args, opts, cwd, commandDesc) {
255
- // Telemetry: every legacy-fallback firing is recorded so the lock+queue removal
256
- // decision rests on observed data — if this never fires over a representative
257
- // window, the whole exclusive-lock path is removable; if it fires, the entries
258
- // name the bd errors that actually need handling (bead construct-nhn5).
259
- try {
260
- const { logBeadsFallback } = await import('./telemetry/beads-fallback.mjs');
261
- logBeadsFallback({ command: commandDesc || args[0] || 'unknown' });
262
- } catch { /* telemetry is best-effort */ }
263
-
264
- // Clean up any stale locks before trying
265
- cleanupStaleLock({ cwd });
266
- cleanupStaleQueue({ cwd });
267
-
268
- // Try to acquire the lock
269
- const startWait = Date.now();
270
- const lock = await acquireLock({
271
- actor: opts.actor,
272
- command: commandDesc,
273
- timeoutSeconds: opts.timeoutSeconds,
274
- cwd,
275
- });
276
-
277
- if (!lock) {
278
- // Failed to get lock; add to queue if allowed
279
- const maxQueueWaitMs = opts.maxQueueWaitSeconds * 1000;
280
- if (Date.now() - startWait > maxQueueWaitMs) {
281
- return {
282
- success: false,
283
- error: `Timeout waiting for lock after ${opts.timeoutSeconds}s`,
284
- queued: false,
285
- method: 'legacy-lock',
286
- };
287
- }
288
-
289
- const queueEntry = enqueueRequest({
290
- actor: opts.actor,
291
- command: commandDesc,
292
- args,
293
- cwd,
294
- });
295
-
296
- if (!opts.silent) {
297
- console.error(`[beads] ↘ Added to queue (position ${readQueue({ cwd }).length})`);
298
- }
299
-
300
- // Wait in queue, polling periodically
301
- while (Date.now() - startWait < opts.maxQueueWaitSeconds * 1000) {
302
- await new Promise((resolve) => setTimeout(resolve, opts.queuePollMs));
303
-
304
- cleanupStaleLock({ cwd });
305
- const status = getLockStatus({ cwd });
306
- if (!status.locked) {
307
- // Lock free, remove from queue and try again
308
- removeFromQueue(queueEntry.timestamp || queueEntry.pid, { cwd });
309
- return await runBd(args, { ...opts, silent: true });
310
- }
311
- }
312
-
313
- // Queue timeout
314
- removeFromQueue(queueEntry.timestamp || queueEntry.pid, { cwd });
315
231
  return {
316
232
  success: false,
317
- error: `Timeout in queue after ${opts.maxQueueWaitSeconds}s`,
318
- queued: true,
319
- waitTimeMs: Date.now() - startWait,
320
- method: 'legacy-lock',
233
+ error: result.error,
234
+ attempts: result.attempts,
235
+ method: 'optimistic-locking',
321
236
  };
322
237
  }
323
238
 
324
- const execStart = Date.now();
325
- let result;
326
- try {
327
- result = spawnSync('bd', args, {
239
+ // ==========================================================================
240
+ // PATH 3: Legacy exclusive lock (retired — bead construct-nhn5 stage 2)
241
+ // ==========================================================================
242
+ if (!isRead && !shouldUseOptimisticLocking()) {
243
+ const startTime = Date.now();
244
+ const result = spawnSync('bd', args, {
328
245
  cwd,
329
246
  encoding: 'utf8',
330
247
  env: { ...process.env, BEADS_ACTOR: opts.actor },
331
- killSignal: 'SIGTERM',
332
248
  timeout: Math.max(1, Math.round(opts.commandTimeoutSeconds * 1000)),
333
249
  });
334
- } finally {
335
- releaseLock({ cwd });
336
- }
337
-
338
- const totalTime = Date.now() - startWait;
339
- const execTime = Date.now() - execStart;
340
-
341
- if (!opts.silent) {
342
- console.error(`[beads] ✓ Executed in ${execTime}ms (legacy lock, total: ${totalTime}ms)`);
250
+ const execTime = Date.now() - startTime;
251
+ if (result.error?.code === 'ETIMEDOUT') {
252
+ return {
253
+ success: false,
254
+ error: `bd ${commandDesc} timed out after ${opts.commandTimeoutSeconds}s`,
255
+ exitCode: null,
256
+ execTimeMs: execTime,
257
+ method: 'direct-write',
258
+ };
259
+ }
260
+ return {
261
+ success: result.status === 0 && !result.error,
262
+ output: result.stdout,
263
+ error: result.stderr || result.error?.message,
264
+ exitCode: result.status,
265
+ execTimeMs: execTime,
266
+ method: 'direct-write',
267
+ };
343
268
  }
344
269
 
345
270
  return {
346
- success: result.status === 0 && !result.error,
347
- output: result.stdout,
348
- error: result.error?.code === 'ETIMEDOUT'
349
- ? `bd ${commandDesc} timed out after ${opts.commandTimeoutSeconds}s`
350
- : (result.stderr || result.error?.message),
351
- exitCode: result.status,
352
- waitTimeMs: execStart - startWait,
353
- execTimeMs: execTime,
354
- totalTimeMs: totalTime,
355
- method: 'legacy-lock',
271
+ success: false,
272
+ error: 'Unexpected beads routing state',
273
+ method: 'unknown',
356
274
  };
357
275
  }
358
276
 
@@ -405,9 +323,8 @@ export async function claimIssue(id, options = {}) {
405
323
  if (result.success) {
406
324
  return { success: true, output: JSON.stringify(result.result) };
407
325
  }
408
-
409
- // Fall back to legacy
410
- return runBd(['update', id, '--claim'], { ...options, useOptimisticLocking: false });
326
+
327
+ return runBd(['update', id, '--claim'], { ...options, useOptimisticLocking: true });
411
328
  }
412
329
 
413
330
  /**
@@ -498,7 +415,18 @@ export async function getHumanStatus(cwd = process.cwd()) {
498
415
 
499
416
  // Optimistic locking status
500
417
  out += 'Locking Strategy:\n';
501
- out += ` Mode: ${shouldUseOptimisticLocking() ? 'Optimistic (concurrent reads + versioned writes)' : 'Legacy (exclusive locks)'}\n`;
418
+ out += ` Mode: ${shouldUseOptimisticLocking() ? 'Optimistic (concurrent reads + versioned writes)' : 'Direct writes (optimistic disabled via CONSTRUCT_BEADS_LEGACY_LOCK=1)'}\n`;
419
+ out += ' Exclusive file-lock fallback: retired (construct-nhn5)\n';
420
+
421
+ try {
422
+ const { readBeadsFallbacks } = await import('./telemetry/beads-fallback.mjs');
423
+ const history = readBeadsFallbacks();
424
+ if (history.length > 0) {
425
+ out += ` Historical fallback log entries: ${history.length} (~/.cx/beads-fallback.jsonl)\n`;
426
+ }
427
+ } catch {
428
+ /* optional archaeology */
429
+ }
502
430
 
503
431
  // Try to get stats if available
504
432
  try {
@@ -280,7 +280,7 @@ export async function runBeadsOperation(operation, options = {}) {
280
280
  type = 'read', // 'read' | 'write' | 'claim' | 'update'
281
281
  beadId,
282
282
  execute,
283
- fallbackToLegacy = true,
283
+ fallbackToLegacy = false,
284
284
  cwd = process.cwd(),
285
285
  } = options;
286
286
 
@@ -302,16 +302,9 @@ export async function runBeadsOperation(operation, options = {}) {
302
302
  if (result.success || !fallbackToLegacy) {
303
303
  return result;
304
304
  }
305
-
306
- // Fall back to legacy locking on persistent failure
307
- console.error(`[beads] Optimistic locking failed, falling back to legacy lock: ${result.error}`);
308
- }
309
-
310
- // Legacy path: use the file-based lock
311
- if (fallbackToLegacy) {
312
- const { runBd } = await import('./beads-client.mjs');
313
- return runBd([operation], { cwd });
305
+
306
+ return { success: false, error: result.error || 'Optimistic write failed' };
314
307
  }
315
-
316
- return { success: false, error: 'Operation failed and fallback disabled' };
308
+
309
+ return { success: false, error: 'Operation failed' };
317
310
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * lib/brand-fonts.mjs — bundled Space Grotesk / JetBrains Mono paths and PPTX font embedding.
3
+ *
4
+ * PDF/Typst reads TTF from templates/distribution/fonts/. HTML/deck templates load
5
+ * the same families from Google Fonts. PPTX uses pptx-embed-fonts when available so
6
+ * PowerPoint renders the bundled faces without a local install. Space Grotesk ships
7
+ * as a single weight-axis variable file; JetBrains Mono as discrete cuts.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { createRequire } from 'node:module';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { BRAND_TOKENS } from './brand-tokens.mjs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ export const BRAND_SANS_FAMILY = BRAND_TOKENS.typography.fontSans;
20
+ export const BRAND_MONO_FAMILY = BRAND_TOKENS.typography.fontMono;
21
+
22
+ export const BUNDLED_SANS_FILES = Object.freeze([
23
+ 'SpaceGrotesk-Variable.ttf',
24
+ ]);
25
+
26
+ export const BUNDLED_MONO_FILES = Object.freeze([
27
+ 'JetBrainsMono-Regular.ttf',
28
+ 'JetBrainsMono-Medium.ttf',
29
+ 'JetBrainsMono-SemiBold.ttf',
30
+ ]);
31
+
32
+ export function distributionFontsDir(repoRoot = REPO_ROOT) {
33
+ return path.join(repoRoot, 'templates', 'distribution', 'fonts');
34
+ }
35
+
36
+ export function bundledSansFontPaths(repoRoot = REPO_ROOT) {
37
+ const dir = distributionFontsDir(repoRoot);
38
+ return BUNDLED_SANS_FILES.map((file) => path.join(dir, file)).filter((p) => fs.existsSync(p));
39
+ }
40
+
41
+ export function bundledMonoFontPaths(repoRoot = REPO_ROOT) {
42
+ const dir = distributionFontsDir(repoRoot);
43
+ return BUNDLED_MONO_FILES.map((file) => path.join(dir, file)).filter((p) => fs.existsSync(p));
44
+ }
45
+
46
+ export function createPptxGenerator() {
47
+ const PptxGenJS = require('pptxgenjs');
48
+ try {
49
+ const { withPPTXEmbedFonts } = require('pptx-embed-fonts/pptxgenjs');
50
+ return { PptxClass: withPPTXEmbedFonts(PptxGenJS), embedFonts: true };
51
+ } catch {
52
+ return { PptxClass: PptxGenJS, embedFonts: false };
53
+ }
54
+ }
55
+
56
+ async function embedFamilyInPptx(pptx, family, paths) {
57
+ let count = 0;
58
+ for (const fontPath of paths) {
59
+ const buf = fs.readFileSync(fontPath);
60
+ await pptx.addFont({
61
+ fontFace: family,
62
+ fontFile: buf,
63
+ fontType: 'ttf',
64
+ });
65
+ count += 1;
66
+ }
67
+ return count;
68
+ }
69
+
70
+ export async function embedBundledSansInPptx(pptx, repoRoot = REPO_ROOT) {
71
+ if (typeof pptx?.addFont !== 'function') return { embedded: false, count: 0 };
72
+ const count = await embedFamilyInPptx(pptx, BRAND_SANS_FAMILY, bundledSansFontPaths(repoRoot));
73
+ return { embedded: count > 0, count };
74
+ }
75
+
76
+ export async function embedBundledMonoInPptx(pptx, repoRoot = REPO_ROOT) {
77
+ if (typeof pptx?.addFont !== 'function') return { embedded: false, count: 0 };
78
+ const count = await embedFamilyInPptx(pptx, BRAND_MONO_FAMILY, bundledMonoFontPaths(repoRoot));
79
+ return { embedded: count > 0, count };
80
+ }
81
+
82
+ export function googleFontsSansHref() {
83
+ return 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap';
84
+ }
85
+
86
+ export function pdfUsesBundledBrandSans(pdfPath) {
87
+ try {
88
+ const text = fs.readFileSync(pdfPath).toString('latin1');
89
+ return /SpaceGrotesk|Space Grotesk|JetBrainsMono|JetBrains Mono/.test(text);
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * lib/brand-prose.mjs — marketing voice, retired typography, and Construct naming lint.
3
+ *
4
+ * Shared by scripts/audit/03d-brand.mjs, lib/hooks/brand-prose-lint.mjs, and
5
+ * `construct lint:brand`. Scans markdown prose only (skips fenced code and inline code).
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ export const MARKETING_VOICE_RE =
12
+ /\b(robust|enterprise-grade|best-in-class|world-class|cutting-edge|powerful|battle-tested|seamless)\b/i;
13
+
14
+ export const RETIRED_FONT_RE =
15
+ /Plus\s+Jakarta|PlusJakarta|Geist(?:Mono)?|IBM\s+Plex|Libertinus|font-family:\s*["']?Inter["']?/i;
16
+
17
+ const CLI_SUBCOMMANDS =
18
+ 'doctor|init|sync|dev|stop|status|intake|chat|install|drop|search|prune|upgrade|config|list|graph|beads|export|publish|artifact|demo|oracle|ci|acp|profile|models|uninstall|reconcile|embed|ingest|classify|workflow|capability';
19
+
20
+ const PRODUCT_NOUN_PHRASES =
21
+ /\bConstruct (?:install|MCP server|MCP tools?|workflow|chat|package|repo|surfaces?|integration|orchestrator|daemon|specialists?|persona|brand|tooling|standards|config|adapters?|platform|version|release|hooks?|memory|loop|Dashboard|Chat|capability)\b/i;
22
+
23
+ const UNBACKTICKED_CLI_RE = new RegExp(
24
+ `(?<![\`~/])construct\\s+(${CLI_SUBCOMMANDS})(?:\\s+[\\w:-]+)*\\b`,
25
+ 'i',
26
+ );
27
+
28
+ const MISCAPITALIZED_CLI_RE = new RegExp(
29
+ `\\bConstruct\\s+(${CLI_SUBCOMMANDS})(?:\\s+[\\w:-]+)*\\b`,
30
+ );
31
+
32
+ const MARKETING_SCAN_ROOTS = ['docs', 'skills', 'specialists', 'personas', 'templates', 'rules'];
33
+ const MARKETING_SCAN_EXTS = ['.md', '.mdx'];
34
+ const FONT_SCAN_EXTS = ['.mjs', '.js', '.md', '.mdx', '.json', '.html', '.css', '.tsx', '.ts', '.jsx', '.typ'];
35
+
36
+ const EXCLUDE_PATH =
37
+ /(node_modules|\.git|audit-artifacts|scripts\/audit|CHANGELOG\.md|fonts\/legacy\/|\.next\/|\/out\/|tests\/e2e\/reports\/|package-lock\.json)/;
38
+
39
+ const DOC_EXEMPT =
40
+ /(?:^|\/)(docs\/STYLE\.md|docs\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
41
+
42
+ const HOOK_SCOPED =
43
+ /^(docs\/|skills\/|specialists\/|personas\/|templates\/|rules\/|apps\/(dashboard|docs|chat)\/)/;
44
+
45
+ export function isBrandHookPath(relPath) {
46
+ if (!relPath || EXCLUDE_PATH.test(relPath) || DOC_EXEMPT.test(relPath)) return false;
47
+ return HOOK_SCOPED.test(relPath.replace(/\\/g, '/'));
48
+ }
49
+
50
+ export function splitProseLines(content) {
51
+ const prose = [];
52
+ let inFence = false;
53
+ const lines = content.split('\n');
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i];
56
+ const trimmed = line.trim();
57
+ if (trimmed.startsWith('```')) {
58
+ inFence = !inFence;
59
+ continue;
60
+ }
61
+ if (inFence) continue;
62
+ prose.push({ line: i + 1, text: line });
63
+ }
64
+ return prose;
65
+ }
66
+
67
+ function stripInlineCode(text) {
68
+ return text.replace(/`[^`]*`/g, '');
69
+ }
70
+
71
+ function isMarketingAllowlisted(relPath, lineText) {
72
+ if (/skills\/roles\//.test(relPath) && /["']robust["']|POUR|Symptom/i.test(lineText)) return true;
73
+ if (/<!--/.test(lineText)) return true;
74
+ if (/\b(WCAG|Perceivable, Operable)/i.test(lineText) && /\bRobust\b/.test(lineText)) return true;
75
+ return false;
76
+ }
77
+
78
+ function isNamingAllowlisted(relPath, lineText) {
79
+ const trimmed = lineText.trim();
80
+ if (/^#+\s+construct\s+/i.test(trimmed)) return true;
81
+ if (/^title:\s/i.test(trimmed)) return true;
82
+ if (/^description:\s/i.test(trimmed)) return true;
83
+ if (/node\s+bin\/construct/.test(lineText)) return true;
84
+ if (/CONSTRUCT_[A-Z_]+=/.test(lineText) && /construct\s+sync/.test(lineText)) return true;
85
+ if ((lineText.match(/\|/g) || []).length >= 2) return true;
86
+ if (/cli:construct/.test(lineText)) return true;
87
+ if (/mcp:.*construct/.test(lineText)) return true;
88
+ if (/\{\s*label:\s*['`]construct/.test(lineText)) return true;
89
+ if (PRODUCT_NOUN_PHRASES.test(lineText)) return true;
90
+ if (/verified_by:\s*construct\b/.test(lineText)) return true;
91
+ if (/construct_schema_migrations/.test(lineText)) return true;
92
+ if (/non-Construct/.test(lineText)) return true;
93
+ return false;
94
+ }
95
+
96
+ export function lintMarketingVoiceLine(relPath, lineNumber, lineText) {
97
+ const prose = stripInlineCode(lineText);
98
+ if (!MARKETING_VOICE_RE.test(prose)) return null;
99
+ if (isMarketingAllowlisted(relPath, lineText)) return null;
100
+ const match = prose.match(MARKETING_VOICE_RE);
101
+ return {
102
+ kind: 'marketing-voice',
103
+ line: lineNumber,
104
+ token: match?.[0] ?? 'marketing',
105
+ text: lineText.trim().slice(0, 140),
106
+ };
107
+ }
108
+
109
+ export function lintRetiredFontLine(relPath, lineNumber, lineText) {
110
+ if (!RETIRED_FONT_RE.test(lineText)) return null;
111
+ return {
112
+ kind: 'retired-font',
113
+ line: lineNumber,
114
+ text: lineText.trim().slice(0, 140),
115
+ };
116
+ }
117
+
118
+ export function lintConstructNamingLine(relPath, lineNumber, lineText) {
119
+ if (!/\.mdx?$/.test(relPath)) return null;
120
+ if (isNamingAllowlisted(relPath, lineText)) return null;
121
+ const prose = stripInlineCode(lineText);
122
+ const miscap = prose.match(MISCAPITALIZED_CLI_RE);
123
+ if (miscap) {
124
+ return {
125
+ kind: 'construct-naming',
126
+ line: lineNumber,
127
+ text: lineText.trim().slice(0, 140),
128
+ detail: `Use lowercase CLI form: \`construct ${miscap[1]}\``,
129
+ };
130
+ }
131
+ if (/^\s*\d+\.\s/.test(lineText) || /^\s*-\s/.test(lineText)) {
132
+ const unback = prose.match(UNBACKTICKED_CLI_RE);
133
+ if (unback) {
134
+ return {
135
+ kind: 'construct-naming',
136
+ line: lineNumber,
137
+ text: lineText.trim().slice(0, 140),
138
+ detail: `Backtick CLI invocations: \`construct ${unback[1]}...\``,
139
+ };
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ export function lintMarkdownBrand(content, { relPath = '', checks = ['marketing', 'naming', 'font'] } = {}) {
146
+ const violations = [];
147
+ for (const { line, text } of splitProseLines(content)) {
148
+ if (checks.includes('marketing')) {
149
+ const hit = lintMarketingVoiceLine(relPath, line, text);
150
+ if (hit) violations.push(hit);
151
+ }
152
+ if (checks.includes('naming')) {
153
+ const hit = lintConstructNamingLine(relPath, line, text);
154
+ if (hit) violations.push(hit);
155
+ }
156
+ if (checks.includes('font')) {
157
+ const hit = lintRetiredFontLine(relPath, line, text);
158
+ if (hit) violations.push(hit);
159
+ }
160
+ }
161
+ return violations;
162
+ }
163
+
164
+ function walk(dir, exts, out = []) {
165
+ if (!fs.existsSync(dir)) return out;
166
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
167
+ const full = path.join(dir, e.name);
168
+ if (EXCLUDE_PATH.test(full)) continue;
169
+ if (e.isDirectory()) walk(full, exts, out);
170
+ else if (exts.some((x) => e.name.endsWith(x))) out.push(full);
171
+ }
172
+ return out;
173
+ }
174
+
175
+ export function scanRepoBrandProse(rootDir, { checks = ['marketing', 'naming', 'font'] } = {}) {
176
+ const hits = [];
177
+ for (const base of MARKETING_SCAN_ROOTS) {
178
+ const abs = path.join(rootDir, base);
179
+ for (const file of walk(abs, [...MARKETING_SCAN_EXTS, ...(checks.includes('font') ? FONT_SCAN_EXTS : [])])) {
180
+ const rel = path.relative(rootDir, file).replace(/\\/g, '/');
181
+ if (DOC_EXEMPT.test(rel)) continue;
182
+ const ext = path.extname(file);
183
+ const fileChecks = checks.filter((c) => c !== 'font' || FONT_SCAN_EXTS.includes(ext));
184
+ const content = fs.readFileSync(file, 'utf8');
185
+ for (const v of lintMarkdownBrand(content, { relPath: rel, checks: fileChecks })) {
186
+ hits.push({ file: rel, ...v });
187
+ }
188
+ }
189
+ }
190
+ return hits;
191
+ }
192
+
193
+ export function lintFile(absPath, { rootDir = process.cwd() } = {}) {
194
+ const rel = path.relative(rootDir, absPath).replace(/\\/g, '/');
195
+ if (!isBrandHookPath(rel)) return { rel, violations: [] };
196
+ const ext = path.extname(absPath).toLowerCase();
197
+ if (!['.md', '.mdx', '.html', '.css', '.tsx', '.ts', '.jsx', '.typ'].includes(ext)) {
198
+ return { rel, violations: [] };
199
+ }
200
+ const content = fs.readFileSync(absPath, 'utf8');
201
+ const checks = ['.md', '.mdx'].includes(ext) ? ['marketing', 'naming', 'font'] : ['font'];
202
+ return { rel, violations: lintMarkdownBrand(content, { relPath: rel, checks }) };
203
+ }
204
+
205
+ export function formatBrandViolations(results) {
206
+ const lines = [];
207
+ for (const { rel, violations } of results) {
208
+ for (const v of violations) {
209
+ const detail = v.detail ? ` — ${v.detail}` : '';
210
+ lines.push(`${rel}:${v.line} [${v.kind}] ${v.text}${detail}`);
211
+ }
212
+ }
213
+ return lines.join('\n');
214
+ }