nexakit 0.0.0-stage → 0.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 (438) hide show
  1. package/.claude-plugin/marketplace.json +28 -0
  2. package/LICENSE +26 -0
  3. package/README.md +79 -2
  4. package/agents/claude/docs-researcher.md +9 -0
  5. package/agents/claude/implementation-tutor.md +9 -0
  6. package/agents/claude/project-explorer.md +9 -0
  7. package/agents/claude/quality-reviewer.md +9 -0
  8. package/agents/claude/requirements-analyst.md +9 -0
  9. package/agents/claude/solution-architect.md +9 -0
  10. package/agents/codex/docs-researcher.toml +10 -0
  11. package/agents/codex/implementation-tutor.toml +10 -0
  12. package/agents/codex/project-explorer.toml +10 -0
  13. package/agents/codex/quality-reviewer.toml +10 -0
  14. package/agents/codex/requirements-analyst.toml +11 -0
  15. package/agents/codex/solution-architect.toml +10 -0
  16. package/assets/brand/README.md +38 -0
  17. package/assets/brand/agent-toolkit-mark.png +0 -0
  18. package/assets/brand/agent-toolkit-mark.svg +13 -0
  19. package/assets/brand/agent-toolkit-tui-hero.png +0 -0
  20. package/assets/brand/nexakit-readme-hero.png +0 -0
  21. package/assets/brand/nexakit-readme-hero.svg +21 -0
  22. package/assets/runtime/package-lock.json +1425 -0
  23. package/bin/agent-task.js +14 -0
  24. package/bin/agent-toolkit-gemini.js +13 -0
  25. package/bin/agent-toolkit-inbox.js +14 -0
  26. package/bin/agent-toolkit-mcp.js +17 -0
  27. package/bin/agent-toolkit.js +13 -0
  28. package/bin/nexakit.js +13 -0
  29. package/bin/project-memory-mcp.js +22 -0
  30. package/bin/project-memory.js +7 -0
  31. package/catalogs/skills/api-design-principles/LICENSE.txt +21 -0
  32. package/catalogs/skills/api-design-principles/SKILL.md +110 -0
  33. package/catalogs/skills/api-design-principles/assets/api-design-checklist.md +155 -0
  34. package/catalogs/skills/api-design-principles/assets/rest-api-template.py +185 -0
  35. package/catalogs/skills/api-design-principles/references/details.md +413 -0
  36. package/catalogs/skills/api-design-principles/references/graphql-schema-design.md +589 -0
  37. package/catalogs/skills/api-design-principles/references/rest-best-practices.md +408 -0
  38. package/catalogs/skills/nodejs-backend-patterns/LICENSE.txt +21 -0
  39. package/catalogs/skills/nodejs-backend-patterns/SKILL.md +45 -0
  40. package/catalogs/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  41. package/catalogs/skills/nodejs-backend-patterns/references/details.md +599 -0
  42. package/catalogs/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  43. package/catalogs/skills/supabase-postgres-best-practices/LICENSE.txt +21 -0
  44. package/catalogs/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  45. package/catalogs/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  46. package/catalogs/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  47. package/catalogs/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  48. package/catalogs/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  49. package/catalogs/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  50. package/catalogs/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  51. package/catalogs/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  52. package/catalogs/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  53. package/catalogs/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  54. package/catalogs/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  55. package/catalogs/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  56. package/catalogs/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  57. package/catalogs/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  58. package/catalogs/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  59. package/catalogs/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  60. package/catalogs/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  61. package/catalogs/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  62. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  63. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  64. package/catalogs/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  65. package/catalogs/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  66. package/catalogs/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  67. package/catalogs/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  68. package/catalogs/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  69. package/catalogs/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  70. package/catalogs/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  71. package/catalogs/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  72. package/catalogs/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  73. package/catalogs/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  74. package/catalogs/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  75. package/catalogs/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  76. package/catalogs/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  77. package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  78. package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  79. package/catalogs/skills/vercel-composition-patterns/AGENTS.md +946 -0
  80. package/catalogs/skills/vercel-composition-patterns/LICENSE.txt +27 -0
  81. package/catalogs/skills/vercel-composition-patterns/README.md +60 -0
  82. package/catalogs/skills/vercel-composition-patterns/SKILL.md +89 -0
  83. package/catalogs/skills/vercel-composition-patterns/metadata.json +11 -0
  84. package/catalogs/skills/vercel-composition-patterns/rules/_sections.md +29 -0
  85. package/catalogs/skills/vercel-composition-patterns/rules/_template.md +24 -0
  86. package/catalogs/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
  87. package/catalogs/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
  88. package/catalogs/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
  89. package/catalogs/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
  90. package/catalogs/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
  91. package/catalogs/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
  92. package/catalogs/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
  93. package/catalogs/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
  94. package/catalogs/skills/vercel-react-best-practices/AGENTS.md +3810 -0
  95. package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
  96. package/catalogs/skills/vercel-react-best-practices/README.md +123 -0
  97. package/catalogs/skills/vercel-react-best-practices/SKILL.md +149 -0
  98. package/catalogs/skills/vercel-react-best-practices/metadata.json +15 -0
  99. package/catalogs/skills/vercel-react-best-practices/rules/_sections.md +46 -0
  100. package/catalogs/skills/vercel-react-best-practices/rules/_template.md +28 -0
  101. package/catalogs/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
  102. package/catalogs/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  103. package/catalogs/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  104. package/catalogs/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  105. package/catalogs/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  106. package/catalogs/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
  107. package/catalogs/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
  108. package/catalogs/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  109. package/catalogs/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  110. package/catalogs/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  111. package/catalogs/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
  112. package/catalogs/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
  113. package/catalogs/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  114. package/catalogs/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  115. package/catalogs/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  116. package/catalogs/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  117. package/catalogs/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  118. package/catalogs/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  119. package/catalogs/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  120. package/catalogs/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  121. package/catalogs/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  122. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  123. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  124. package/catalogs/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  125. package/catalogs/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  126. package/catalogs/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  127. package/catalogs/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  128. package/catalogs/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  129. package/catalogs/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  130. package/catalogs/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  131. package/catalogs/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  132. package/catalogs/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
  133. package/catalogs/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  134. package/catalogs/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  135. package/catalogs/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  136. package/catalogs/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  137. package/catalogs/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  138. package/catalogs/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  139. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  140. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  141. package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  142. package/catalogs/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  143. package/catalogs/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  144. package/catalogs/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  145. package/catalogs/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  146. package/catalogs/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  147. package/catalogs/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  148. package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  149. package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  150. package/catalogs/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  151. package/catalogs/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  152. package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  153. package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  154. package/catalogs/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  155. package/catalogs/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  156. package/catalogs/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  157. package/catalogs/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
  158. package/catalogs/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  159. package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
  160. package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  161. package/catalogs/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  162. package/catalogs/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  163. package/catalogs/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  164. package/catalogs/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  165. package/catalogs/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  166. package/catalogs/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
  167. package/catalogs/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
  168. package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  169. package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
  170. package/catalogs/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  171. package/catalogs/skills/webapp-testing/LICENSE.txt +202 -0
  172. package/catalogs/skills/webapp-testing/SKILL.md +96 -0
  173. package/catalogs/skills/webapp-testing/examples/console_logging.py +35 -0
  174. package/catalogs/skills/webapp-testing/examples/element_discovery.py +40 -0
  175. package/catalogs/skills/webapp-testing/examples/static_html_automation.py +33 -0
  176. package/catalogs/skills/webapp-testing/scripts/with_server.py +106 -0
  177. package/catalogs/skills.json +1115 -0
  178. package/docs/skills/evidencias/2026-10-28-evaluacion-inicial-mcp-seguridad.md +118 -0
  179. package/docs/skills/evidencias/2026-10-28-incorporacion-mcp-seguridad.json +152 -0
  180. package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +50 -0
  181. package/package.json +73 -4
  182. package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
  183. package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
  184. package/plugins/nexakit-modelo/hooks/register.js +48 -0
  185. package/plugins/nexakit-modelo/hooks/status.js +8 -0
  186. package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
  187. package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
  188. package/plugins/nexakit-panel/README.md +20 -0
  189. package/plugins/nexakit-panel/hooks/hooks.json +1 -0
  190. package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
  191. package/plugins/nexakit-panel/hooks/panel.js +279 -0
  192. package/plugins/nexakit-panel/hooks/register.tsx +248 -0
  193. package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
  194. package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
  195. package/plugins/nexakit-panel/tests/register.test.ts +167 -0
  196. package/plugins/nexakit-panel/types/index.d.ts +41 -0
  197. package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
  198. package/plugins/nexakit-sesion/README.md +12 -0
  199. package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
  200. package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
  201. package/plugins/nexakit-sesion/hooks/register.js +28 -0
  202. package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
  203. package/plugins/niveles/.claude-plugin/plugin.json +5 -0
  204. package/plugins/niveles/agents/nivel-alto.md +13 -0
  205. package/plugins/niveles/agents/nivel-exigente.md +13 -0
  206. package/plugins/niveles/agents/nivel-ligero.md +13 -0
  207. package/plugins/niveles/agents/nivel-normal.md +13 -0
  208. package/schemas/agent-client-adapter.schema.json +786 -0
  209. package/schemas/agent-client-auth-diagnostic.schema.json +801 -0
  210. package/schemas/agent-client-isolated-variant.schema.json +105 -0
  211. package/schemas/agent-client-isolated-verification.schema.json +99 -0
  212. package/schemas/agent-client-managed-preferences-invocation.schema.json +67 -0
  213. package/schemas/agent-client-managed-preferences.schema.json +63 -0
  214. package/schemas/agent-client-native-diagnostics.schema.json +85 -0
  215. package/schemas/agent-client-native-evaluation.schema.json +171 -0
  216. package/schemas/agent-client-native-preflight.schema.json +90 -0
  217. package/schemas/agent-client-output.schema.json +551 -0
  218. package/schemas/agent-client-runtime-fixture.schema.json +90 -0
  219. package/schemas/agent-client-runtime-local.schema.json +95 -0
  220. package/schemas/agent-client-runtime.schema.json +1288 -0
  221. package/schemas/agent-client-selective-config.schema.json +77 -0
  222. package/schemas/agent-client-supervised-local.schema.json +135 -0
  223. package/schemas/agent-client-supervised.schema.json +186 -0
  224. package/schemas/agent-model-preference.schema.json +573 -0
  225. package/schemas/agent-model-selection.schema.json +311 -0
  226. package/schemas/agent-task-approvals.schema.json +362 -0
  227. package/schemas/agent-task-prepare.schema.json +1324 -0
  228. package/schemas/agent-task-verify.schema.json +198 -0
  229. package/schemas/project-memory-update.schema.json +33 -0
  230. package/schemas/project-memory.schema.json +298 -0
  231. package/schemas/skill-catalog.schema.json +246 -0
  232. package/schemas/task-workflow-plan-output.schema.json +35 -0
  233. package/scripts/prepare-package.js +6 -0
  234. package/scripts/validate-skill-catalog.js +41 -0
  235. package/skills/cerrar-avisens/SKILL.md +12 -0
  236. package/skills/cerrar-memoria/SKILL.md +96 -0
  237. package/skills/cerrar-memoria/references/cierre.md +76 -0
  238. package/skills/decision-records/SKILL.md +27 -0
  239. package/skills/decision-records/agents/openai.yaml +7 -0
  240. package/skills/decision-records/references/adr-template.md +35 -0
  241. package/skills/differential-review/LICENSE.txt +427 -0
  242. package/skills/differential-review/SKILL.md +225 -0
  243. package/skills/differential-review/adversarial.md +203 -0
  244. package/skills/differential-review/agents/openai.yaml +6 -0
  245. package/skills/differential-review/assets/trail-of-bits-mark.svg +1 -0
  246. package/skills/differential-review/methodology.md +234 -0
  247. package/skills/differential-review/patterns.md +300 -0
  248. package/skills/differential-review/reporting.md +369 -0
  249. package/skills/elegir-modelo/SKILL.md +68 -0
  250. package/skills/elegir-modelo/modelos.mjs +108 -0
  251. package/skills/frontend-design/LICENSE.txt +177 -0
  252. package/skills/frontend-design/SKILL.md +71 -0
  253. package/skills/gemini/SKILL.md +52 -0
  254. package/skills/imagen/SKILL.md +12 -0
  255. package/skills/investigar/SKILL.md +11 -0
  256. package/skills/mcp-builder/LICENSE.txt +202 -0
  257. package/skills/mcp-builder/SKILL.md +236 -0
  258. package/skills/mcp-builder/reference/evaluation.md +602 -0
  259. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  260. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  261. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  262. package/skills/mcp-builder/scripts/connections.py +151 -0
  263. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  264. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  265. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  266. package/skills/ordenar-proyecto/SKILL.md +56 -0
  267. package/skills/project-handoff/SKILL.md +53 -0
  268. package/skills/project-handoff/agents/openai.yaml +7 -0
  269. package/skills/project-handoff/references/handoff-template.md +35 -0
  270. package/skills/quality-gate/LICENSE.txt +26 -0
  271. package/skills/quality-gate/SKILL.md +44 -0
  272. package/skills/quality-gate/agents/openai.yaml +7 -0
  273. package/skills/quality-gate/references/review-checklist.md +42 -0
  274. package/skills/revisar/SKILL.md +11 -0
  275. package/skills/revisar-skill/LICENSE-find-skills.txt +26 -0
  276. package/skills/revisar-skill/SKILL.md +94 -0
  277. package/skills/revision-cruzada/SKILL.md +59 -0
  278. package/skills/web-design-guidelines/AGENTS.md +156 -0
  279. package/skills/web-design-guidelines/LICENSE.txt +21 -0
  280. package/skills/web-design-guidelines/SKILL.md +29 -0
  281. package/src/agent-models.js +131 -0
  282. package/src/brand.js +117 -0
  283. package/src/claude-code-runner.js +642 -0
  284. package/src/cli-executable.js +88 -0
  285. package/src/cli.js +778 -0
  286. package/src/client-adapter-plan.js +271 -0
  287. package/src/client-auth-diagnostic-parser.js +211 -0
  288. package/src/client-auth-diagnostic.js +257 -0
  289. package/src/client-isolated-variant.js +106 -0
  290. package/src/client-isolated-verification.js +130 -0
  291. package/src/client-local-platform.js +14 -0
  292. package/src/client-managed-preferences-invocation.js +209 -0
  293. package/src/client-managed-preferences.js +78 -0
  294. package/src/client-native-access.js +134 -0
  295. package/src/client-native-diagnostics.js +253 -0
  296. package/src/client-native-evaluation.js +106 -0
  297. package/src/client-native-preflight-files.js +68 -0
  298. package/src/client-native-preflight.js +95 -0
  299. package/src/client-output-json.js +118 -0
  300. package/src/client-output-parser.js +278 -0
  301. package/src/client-runtime-admission.js +80 -0
  302. package/src/client-runtime-coordinator.js +70 -0
  303. package/src/client-runtime-core.js +92 -0
  304. package/src/client-runtime-fixture-permit.js +20 -0
  305. package/src/client-runtime-fixture-protocol.js +39 -0
  306. package/src/client-runtime-state.js +67 -0
  307. package/src/client-runtime-store-files.js +91 -0
  308. package/src/client-runtime-store.js +239 -0
  309. package/src/client-runtime-supervisor.js +146 -0
  310. package/src/client-selective-config.js +195 -0
  311. package/src/client-selective-files.js +126 -0
  312. package/src/client-supervised-codes.js +32 -0
  313. package/src/client-supervised-contract.js +52 -0
  314. package/src/client-supervised-coordinator.js +36 -0
  315. package/src/client-supervised-permit.js +20 -0
  316. package/src/client-supervised-protocol.js +37 -0
  317. package/src/client-supervised-store.js +167 -0
  318. package/src/client-supervised-supervisor.js +167 -0
  319. package/src/client-supervised.js +160 -0
  320. package/src/codex-cli-runner.js +358 -0
  321. package/src/codex-delivery.js +101 -0
  322. package/src/connect.js +703 -0
  323. package/src/doctor.js +795 -0
  324. package/src/gemini-cli.js +76 -0
  325. package/src/gemini-files.js +45 -0
  326. package/src/gemini-tools.js +249 -0
  327. package/src/init.js +473 -0
  328. package/src/install-lock.js +127 -0
  329. package/src/install-registry.js +61 -0
  330. package/src/legacy-repair.js +71 -0
  331. package/src/level-agents.js +111 -0
  332. package/src/link-states.js +172 -0
  333. package/src/memory-connection-artifacts.js +192 -0
  334. package/src/memory-connection-descriptor.js +158 -0
  335. package/src/memory-connection-diagnostic.js +177 -0
  336. package/src/memory-connection-files.js +191 -0
  337. package/src/memory-connection-manifest.js +118 -0
  338. package/src/memory-connection-package.js +176 -0
  339. package/src/memory-connection-plan.js +211 -0
  340. package/src/memory-connection-skill.js +211 -0
  341. package/src/memory-connection-toml.js +153 -0
  342. package/src/memory-locator.js +118 -0
  343. package/src/memory-mcp-policy.js +181 -0
  344. package/src/memory-mcp-server.js +166 -0
  345. package/src/memory-mcp-transport.js +59 -0
  346. package/src/missing-dependencies.js +21 -0
  347. package/src/model-command.js +147 -0
  348. package/src/model-preference.js +235 -0
  349. package/src/model-selection.js +368 -0
  350. package/src/native/managed-preferences.c +100 -0
  351. package/src/native/managed-preferences.h +17 -0
  352. package/src/package-lock-path.js +13 -0
  353. package/src/panel-cli.js +182 -0
  354. package/src/peer-handoff.js +27 -0
  355. package/src/peer-inbox-cli.js +211 -0
  356. package/src/peer-review-mcp.js +210 -0
  357. package/src/peer-review-store.js +532 -0
  358. package/src/platform.js +24 -0
  359. package/src/project-root.js +41 -0
  360. package/src/project-signals.js +339 -0
  361. package/src/recommend-report.js +77 -0
  362. package/src/review-instructions.js +211 -0
  363. package/src/review-policy.js +35 -0
  364. package/src/revision-adapters.js +157 -0
  365. package/src/revision-context.js +86 -0
  366. package/src/revision-isolation.js +222 -0
  367. package/src/revision-process.js +103 -0
  368. package/src/revision-provenance.js +89 -0
  369. package/src/revision-snapshot.js +120 -0
  370. package/src/revision.js +242 -0
  371. package/src/runtime-fixtures/runtime-supervisor/child.cjs +3 -0
  372. package/src/runtime-fixtures/runtime-supervisor/fixture.cjs +49 -0
  373. package/src/runtime-fixtures/supervised/child.cjs +3 -0
  374. package/src/runtime-fixtures/supervised/fixture.cjs +47 -0
  375. package/src/sensitive-data.js +21 -0
  376. package/src/setup-apply.js +231 -0
  377. package/src/setup-plan.js +231 -0
  378. package/src/setup-state.js +277 -0
  379. package/src/setup-tui.js +555 -0
  380. package/src/skill-catalog-add.js +91 -0
  381. package/src/skill-catalog-audit.js +68 -0
  382. package/src/skill-catalog.js +170 -0
  383. package/src/skill-files.js +39 -0
  384. package/src/skill-recommendations.js +1673 -0
  385. package/src/skill-scan-report.js +14 -0
  386. package/src/skill-scan.js +178 -0
  387. package/src/stable-command.js +53 -0
  388. package/src/stable-install.js +222 -0
  389. package/src/status.js +200 -0
  390. package/src/superpowers.js +133 -0
  391. package/src/sync-apply.js +227 -0
  392. package/src/sync-files.js +109 -0
  393. package/src/sync-inspect.js +394 -0
  394. package/src/sync-ledger.js +94 -0
  395. package/src/sync-markdown.js +151 -0
  396. package/src/sync-proposal.js +84 -0
  397. package/src/sync-record.js +459 -0
  398. package/src/sync.js +360 -0
  399. package/src/task-approvals.js +367 -0
  400. package/src/task-cli.js +79 -0
  401. package/src/task-external-verifier.js +584 -0
  402. package/src/task-prepare-files.js +115 -0
  403. package/src/task-prepare.js +180 -0
  404. package/src/task-verify-cli.js +67 -0
  405. package/src/task-verify-files.js +188 -0
  406. package/src/task-verify.js +118 -0
  407. package/src/task-workflow-mcp.js +544 -0
  408. package/src/task-workflow-store.js +254 -0
  409. package/src/terminal-text.js +68 -0
  410. package/src/toolkit-cli.js +543 -0
  411. package/src/toolkit-commands-guide.js +107 -0
  412. package/src/toolkit-setup.js +614 -0
  413. package/src/usage-metrics.js +43 -0
  414. package/templates/agent-team/01-encargo.md +158 -0
  415. package/templates/agent-team/02-propuesta.md +132 -0
  416. package/templates/agent-team/03-revision.md +150 -0
  417. package/templates/agent-team/README.md +213 -0
  418. package/templates/model-inventory/01-inventario.md +154 -0
  419. package/templates/model-inventory/README.md +115 -0
  420. package/templates/project-memory/README.md +67 -0
  421. package/templates/project-memory/project-memory.example.json +53 -0
  422. package/templates/project-memory/skill/project-id/SKILL.md +37 -0
  423. package/templates/project-memory/skill/project-id/references/project-profile.md +44 -0
  424. package/templates/project-memory/vault/00-INICIO/Estado actual.md +44 -0
  425. package/templates/project-memory/vault/00-INICIO/Fuentes de verdad.md +30 -0
  426. package/templates/project-memory/vault/00-INICIO/Inicio del proyecto.md +44 -0
  427. package/templates/project-memory/vault/00-INICIO/Mantenimiento de la memoria.md +26 -0
  428. package/templates/project-memory/vault/00-INICIO/Mapa de la memoria.md +33 -0
  429. package/templates/project-memory/vault/00-INICIO/Registro de sincronizaci/303/263n.md" +28 -0
  430. package/templates/project-memory/vault/01-PRODUCTO/Producto.md +16 -0
  431. package/templates/project-memory/vault/02-ARQUITECTURA/Arquitectura.md +16 -0
  432. package/templates/project-memory/vault/03-IMPLEMENTACION/Implementaci/303/263n.md +15 -0
  433. package/templates/project-memory/vault/04-DECISIONES/Decisiones.md +19 -0
  434. package/templates/project-memory/vault/05-ENTREGAS/Entregas.md +12 -0
  435. package/templates/project-memory/vault/06-OPERACION/Operaci/303/263n.md +15 -0
  436. package/templates/project-memory/vault/07-AGENTES/Protocolo de agentes.md +33 -0
  437. package/templates/project-memory/vault/08-RECURSOS/Recursos.md +12 -0
  438. package/templates/project-memory/vault/09-ARCHIVO/README.md +5 -0
@@ -0,0 +1,394 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { diagnoseMemory } from "./doctor.js";
6
+ import { parseGitHubRepository, readManifest, readReviewedPullRequests, validateAutomation } from "./sync.js";
7
+ import { containsSensitiveData, isCredentialPath } from "./sensitive-data.js";
8
+
9
+ export const INSPECT_LIMITS = Object.freeze({
10
+ callMs: 30_000, totalMs: 90_000, responseBytes: 5 * 1024 * 1024,
11
+ outputBytes: 1024 * 1024, filePages: 10, checkPages: 10, checks: 500, notes: 20
12
+ });
13
+ const SHA = /^[a-f0-9]{40}$/i;
14
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
15
+ const ADVERSE = new Set(["failure", "error", "cancelled", "timed_out", "action_required", "startup_failure", "stale"]);
16
+ const CANONICAL_DIRS = ["01-PRODUCTO", "02-ARQUITECTURA", "03-IMPLEMENTACION", "04-DECISIONES", "05-ENTREGAS", "06-OPERACION"];
17
+
18
+ export class SyncInspectionError extends Error {
19
+ constructor(code, message) {
20
+ super(message);
21
+ this.name = "SyncInspectionError";
22
+ this.code = code;
23
+ this.details = [message];
24
+ }
25
+ }
26
+
27
+ export function inspectionErrorReport(error) {
28
+ const safe = error instanceof SyncInspectionError ? error : new SyncInspectionError("inspection-error", "No se pudo completar la inspección de forma segura.");
29
+ return { command: "sync inspect", status: "error", code: safe.code, details: safe.details };
30
+ }
31
+
32
+ export async function inspectMemorySync(options = {}, context = {}) {
33
+ const prNumber = positiveInteger(options.pr, Number.MAX_SAFE_INTEGER, "--pr");
34
+ const maxFiles = positiveInteger(options.maxFiles ?? 200, 1000, "--max-files");
35
+ const clock = context.clock ?? Date.now;
36
+ const started = clock();
37
+ const warnings = [];
38
+ const warn = (code, message) => {
39
+ if (!warnings.some((item) => item.code === code && item.message === message)) warnings.push({ code, message });
40
+ };
41
+ const doctor = await (context.diagnoseMemory ?? diagnoseMemory)(path.resolve(options.targetPath ?? process.cwd()), {
42
+ manifestPath: options.manifestPath ? path.resolve(options.manifestPath) : undefined,
43
+ schemaPath: context.schemaPath
44
+ }).catch(() => { throw new SyncInspectionError("doctor-error", "No se pudo diagnosticar la memoria."); });
45
+ if (doctor.status === "error" || !doctor.manifestPath || !doctor.vaultPath) {
46
+ throw new SyncInspectionError("doctor-error", "La memoria no superó project-memory doctor.");
47
+ }
48
+ if (doctor.status === "warning" || doctor.checks?.some((item) => item.level === "warning")) {
49
+ warn("doctor-warning", "Doctor informó advertencias; revisa el diagnóstico antes de cerrar el trabajo.");
50
+ }
51
+ let manifest, repository, reviewed;
52
+ const ledgerPath = path.join(doctor.vaultPath, "00-INICIO/Registro de sincronización.md");
53
+ try {
54
+ manifest = readManifest(doctor.manifestPath);
55
+ validateAutomation(manifest);
56
+ repository = parseGitHubRepository(manifest.source?.remoteUrl);
57
+ reviewed = readReviewedPullRequests(ledgerPath, repository);
58
+ } catch {
59
+ throw new SyncInspectionError("configuration-error", "Comprueba el manifiesto, la automatización y la tabla del registro.");
60
+ }
61
+ const branch = manifest.automation.reviewBranch;
62
+ if ([repository, branch, ledgerPath].some((value) => typeof value !== "string" || CONTROL.test(value) || containsSensitiveData(value))) {
63
+ throw new SyncInspectionError("configuration-error", "La configuración contiene valores que no pueden emitirse de forma segura.");
64
+ }
65
+ const query = createQuery(context.runCommand ?? runExternalCommand, clock, started);
66
+ const raw = await query(["pr", "view", String(prNumber), "--repo", `github.com/${repository}`, "--json", "number,url,title,body,state,baseRefName,headRefOid,mergedAt,mergeCommit,changedFiles"]);
67
+ validatePullRequest(raw, prNumber, repository, branch);
68
+ const safeText = (value, limit, field) => {
69
+ if (value === null || value === undefined) return null;
70
+ if (containsSensitiveData(value)) {
71
+ warn("sensitive-field", `Se omitió un campo sensible: ${field}.`);
72
+ return null;
73
+ }
74
+ // Keep line breaks in descriptions, remove terminal control characters.
75
+ const clean = value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
76
+ if (clean !== value) warn("text-controls", "Se retiraron caracteres de control de texto externo.");
77
+ if (clean.length > limit) warn("text-limit", `Texto limitado: ${field}.`);
78
+ return clean.slice(0, limit);
79
+ };
80
+ const safeUrl = (value) => {
81
+ if (value === null || value === undefined) return null;
82
+ const result = repositoryUrl(value, repository);
83
+ if (!result) warn("unsafe-reference", "Se omitió una referencia externa o insegura.");
84
+ return result;
85
+ };
86
+ const pullRequest = {
87
+ number: prNumber, url: raw.url, title: safeText(raw.title, 300, "title"),
88
+ description: safeText(raw.body, 4000, "description"), baseBranch: branch,
89
+ mergedAt: new Date(raw.mergedAt).toISOString(), headCommit: raw.headRefOid, mergeCommit: raw.mergeCommit.oid
90
+ };
91
+ const inventory = await collectFiles(query, repository, prNumber, maxFiles, raw.changedFiles, warn);
92
+ const checks = await collectChecks(query, repository, raw.headRefOid, safeText, safeUrl, warn);
93
+ const { candidateAreas, unmappedFiles } = resolveCandidateAreas(doctor.vaultPath, manifest.memory?.layout, inventory.files, warn);
94
+ const report = {
95
+ command: "sync inspect", status: checks.summary.adverse > 0 ? "blocked" : warnings.length ? "partial" : "ready",
96
+ repository, branch, ledgerPath, alreadyReviewed: reviewed.has(prNumber), pullRequest,
97
+ files: inventory.files, checks, candidateAreas, unmappedFiles,
98
+ coverage: { files: inventory.coverage, checks: { complete: checks.complete, pages: checks.pages, received: checks.received }, candidateNotesLimit: INSPECT_LIMITS.notes },
99
+ warnings, observedAt: new Date(clock()).toISOString()
100
+ };
101
+ if (Buffer.byteLength(JSON.stringify(report, null, 2)) > INSPECT_LIMITS.outputBytes) {
102
+ throw new SyncInspectionError("output-limit", "El informe supera el límite seguro de salida; reduce --max-files.");
103
+ }
104
+ return report;
105
+ }
106
+
107
+ function positiveInteger(value, max, label) {
108
+ if (!(typeof value === "number" || typeof value === "string" && /^\d+$/.test(value)) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > max) {
109
+ throw new SyncInspectionError("invalid-option", `${label} requiere un entero positivo dentro del rango permitido.`);
110
+ }
111
+ return Number(value);
112
+ }
113
+
114
+ function createQuery(runCommand, clock, started) {
115
+ return async (args) => {
116
+ const remaining = INSPECT_LIMITS.totalMs - (clock() - started);
117
+ if (remaining <= 0) throw new SyncInspectionError("timeout", "Se agotó el tiempo de inspección.");
118
+ let output;
119
+ try {
120
+ output = await runCommand("gh", args, { timeout: Math.min(INSPECT_LIMITS.callMs, remaining), maxBuffer: INSPECT_LIMITS.responseBytes });
121
+ } catch (error) {
122
+ throw externalQueryError(error);
123
+ }
124
+ if (clock() - started > INSPECT_LIMITS.totalMs) throw new SyncInspectionError("timeout", "Se agotó el tiempo de inspección.");
125
+ if (typeof output !== "string" || Buffer.byteLength(output) > INSPECT_LIMITS.responseBytes) {
126
+ throw new SyncInspectionError("response-limit", "La respuesta supera el límite permitido o no es texto JSON.");
127
+ }
128
+ try { return JSON.parse(output); } catch { throw new SyncInspectionError("invalid-response", "GitHub CLI no devolvió JSON válido."); }
129
+ };
130
+ }
131
+
132
+ function runExternalCommand(command, args, limits) {
133
+ return execFileSync(command, args, { ...limits, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
134
+ }
135
+
136
+ function externalQueryError(error) {
137
+ // Classify errors in memory without ever returning their original streams.
138
+ const diagnostic = String(error?.stderr ?? "");
139
+ let code = "github-unavailable";
140
+ if (error?.code === "ENOENT") code = "command-missing";
141
+ else if (error?.code === "ENOBUFS") code = "response-limit";
142
+ else if (error?.code === "ETIMEDOUT" || error?.signal) code = "timeout";
143
+ else if (/HTTP 429|rate limit/i.test(diagnostic)) code = "github-rate-limit";
144
+ else if (/HTTP 401|gh auth login/i.test(diagnostic)) code = "github-authentication";
145
+ else if (/HTTP 403/i.test(diagnostic)) code = "github-permissions";
146
+ const messages = {
147
+ "command-missing": "GitHub CLI no está disponible.",
148
+ "response-limit": "La respuesta supera el límite permitido.",
149
+ timeout: "La consulta agotó el tiempo permitido.",
150
+ "github-rate-limit": "GitHub alcanzó su límite de consultas; vuelve a intentarlo más tarde.",
151
+ "github-authentication": "Comprueba la autenticación existente de GitHub CLI.",
152
+ "github-permissions": "La cuenta no tiene permisos suficientes para esta consulta.",
153
+ "github-unavailable": "GitHub no está disponible; comprueba autenticación, permisos y límites de API."
154
+ };
155
+ return new SyncInspectionError(code, messages[code]);
156
+ }
157
+
158
+ function repositoryUrl(value, repository) {
159
+ if (typeof value !== "string" || value.length > 2048 || CONTROL.test(value) || containsSensitiveData(value)) return null;
160
+ try {
161
+ const url = new URL(value);
162
+ const prefix = `/${repository}/`.toLowerCase();
163
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || url.search || url.hash || !url.pathname.toLowerCase().startsWith(prefix)) return null;
164
+ return url.href;
165
+ } catch { return null; }
166
+ }
167
+
168
+ function validatePullRequest(raw, number, repository, branch) {
169
+ const validUrl = repositoryUrl(raw?.url, repository);
170
+ if (!raw || raw.number !== number || !validUrl || new URL(validUrl).pathname.toLowerCase() !== `/${repository}/pull/${number}`.toLowerCase()
171
+ || raw.state !== "MERGED" || raw.baseRefName !== branch || typeof raw.mergedAt !== "string" || Number.isNaN(Date.parse(raw.mergedAt))
172
+ || typeof raw.headRefOid !== "string" || !SHA.test(raw.headRefOid)
173
+ || typeof raw.mergeCommit?.oid !== "string" || !SHA.test(raw.mergeCommit.oid)
174
+ || typeof raw.title !== "string" || !(raw.body === null || typeof raw.body === "string")
175
+ || !Number.isSafeInteger(raw.changedFiles) || raw.changedFiles < 0) {
176
+ throw new SyncInspectionError("invalid-pull-request", "El PR no es un cambio fusionado válido del repositorio y rama configurados.");
177
+ }
178
+ }
179
+
180
+ function validRelativePath(value) {
181
+ return typeof value === "string" && value.length > 0 && value.length <= 512
182
+ && !CONTROL.test(value) && !value.startsWith("/") && !/^[a-z]:/i.test(value)
183
+ && !value.includes("\\") && !value.split("/").some((part) => ["", ".", ".."].includes(part));
184
+ }
185
+
186
+ async function collectFiles(query, repository, number, limit, expected, warn) {
187
+ const files = new Map();
188
+ const seen = new Set();
189
+ let pages = 0, received = 0, omitted = 0, complete = expected === 0;
190
+ const statuses = new Set(["added", "removed", "modified", "renamed", "copied", "changed", "unchanged"]);
191
+ try {
192
+ while (!complete && pages < INSPECT_LIMITS.filePages && received < limit) {
193
+ // Keep page size constant: changing it changes GitHub's page offsets.
194
+ const perPage = Math.min(100, limit);
195
+ const values = await query(["api", "--hostname", "github.com", "--method", "GET", `repos/${repository}/pulls/${number}/files?per_page=${perPage}&page=${pages + 1}`, "--jq", "map({filename,previous_filename,status,additions,deletions})"]);
196
+ pages++;
197
+ if (!Array.isArray(values) || values.length > perPage) throw new SyncInspectionError("invalid-files", "Inventario de archivos inválido.");
198
+ const selected = values.slice(0, limit - received);
199
+ received += selected.length;
200
+ for (const value of selected) {
201
+ if (!validRelativePath(value?.filename) || (value.previous_filename != null && !validRelativePath(value.previous_filename))
202
+ || !statuses.has(value.status) || !Number.isSafeInteger(value.additions) || value.additions < 0 || !Number.isSafeInteger(value.deletions) || value.deletions < 0
203
+ || value.status === "renamed" && !value.previous_filename) {
204
+ throw new SyncInspectionError("invalid-files", "El inventario contiene entradas inválidas.");
205
+ }
206
+ if (seen.has(value.filename)) warn("duplicate-files", "GitHub devolvió archivos repetidos; el inventario se deduplicó.");
207
+ seen.add(value.filename);
208
+ if ([value.filename, value.previous_filename].filter(Boolean).some((name) => containsSensitiveData(name) || isCredentialPath(name))) {
209
+ omitted++;
210
+ warn("sensitive-files", "Se omitieron rutas sensibles del inventario y de las candidatas.");
211
+ continue;
212
+ }
213
+ files.set(value.filename, { path: value.filename, previousPath: value.previous_filename ?? null, change: value.status, additions: value.additions, deletions: value.deletions });
214
+ }
215
+ // A short last page cannot prove completeness unless the expected count
216
+ // agrees. Bounded selection may intentionally omit part of a full page.
217
+ complete = seen.size === expected && received === expected && values.length === selected.length;
218
+ if (values.length < perPage || received >= expected) break;
219
+ }
220
+ } catch (error) {
221
+ if (!(error instanceof SyncInspectionError)) throw error;
222
+ warn(`files-${error.code}`, "El inventario no pudo completarse; requiere revisión manual.");
223
+ complete = false;
224
+ }
225
+ if (!complete) warn("files-incomplete", "El inventario está limitado o no coincide con el total del PR.");
226
+ return {
227
+ files: [...files.values()].sort((a, b) => a.path.localeCompare(b.path, "en")),
228
+ coverage: { expected, received, unique: seen.size, visible: files.size, omitted, pages, limit, complete }
229
+ };
230
+ }
231
+
232
+ async function collectChecks(query, repository, commit, safeText, safeUrl, warn) {
233
+ const results = new Map();
234
+ let pages = 0, received = 0, complete = true;
235
+ async function collect(kind) {
236
+ let page = 1, count = 0;
237
+ while (pages < INSPECT_LIMITS.checkPages && received < INSPECT_LIMITS.checks) {
238
+ const perPage = 100;
239
+ const endpoint = kind === "check-run" ? `check-runs?filter=latest&per_page=${perPage}&page=${page}` : `status?per_page=${perPage}&page=${page}`;
240
+ const jq = kind === "check-run" ? "{total_count,check_runs:[.check_runs[]|{id,name,status,conclusion,head_sha,html_url,completed_at,started_at,app:{id:.app.id}}]}" : "{sha,total_count,state,statuses:[.statuses[]|{context,state,target_url,updated_at}]}";
241
+ const response = await query(["api", "--hostname", "github.com", "--method", "GET", `repos/${repository}/commits/${commit}/${endpoint}`, "--jq", jq]);
242
+ pages++;
243
+ const values = kind === "check-run" ? response?.check_runs : response?.statuses;
244
+ if (!Number.isSafeInteger(response?.total_count) || response.total_count < 0 || !Array.isArray(values) || values.length > perPage || kind === "commit-status" && (response.sha !== commit || !["pending", "success", "failure", "error"].includes(response.state))) {
245
+ throw new SyncInspectionError("invalid-checks", "La respuesta de comprobaciones no es válida para el commit consultado.");
246
+ }
247
+ // Combined state remains useful when individual contexts are truncated.
248
+ if (kind === "commit-status" && ["failure", "error"].includes(response.state)) {
249
+ results.set("combined-status", { kind: "commit-status-summary", name: "Estado combinado de GitHub", state: response.state, conclusion: response.state, url: null, commit, timestamp: null });
250
+ }
251
+ const selected = values.slice(0, INSPECT_LIMITS.checks - received);
252
+ count += selected.length;
253
+ received += selected.length;
254
+ for (const value of selected) {
255
+ const rawName = kind === "check-run" ? value?.name : value?.context;
256
+ const validRun = kind !== "check-run" || (typeof value?.status === "string" && value.head_sha === commit && Number.isSafeInteger(value.id) && value.id > 0 && Number.isSafeInteger(value.app?.id) && value.app.id > 0);
257
+ const validStatus = kind !== "commit-status" || ["success", "failure", "error", "pending"].includes(value?.state);
258
+ if (typeof rawName !== "string" || !validRun || !validStatus) {
259
+ throw new SyncInspectionError("invalid-checks", "Una comprobación no pertenece al commit o contiene datos inválidos.");
260
+ }
261
+ let state = kind === "check-run" ? value.status : value.state;
262
+ let conclusion = kind === "check-run" ? value.conclusion : value.state;
263
+ if (conclusion !== null && typeof conclusion !== "string") throw new SyncInspectionError("invalid-checks", "La conclusión de una comprobación no es válida.");
264
+ if (!["queued", "in_progress", "waiting", "requested", "pending", "completed", "success", "failure", "error"].includes(state)) {
265
+ state = "unknown";
266
+ warn("checks-unknown-state", "Se recibió un estado de comprobación desconocido.");
267
+ }
268
+ if (conclusion !== null && !["success", "failure", "error", "pending", "neutral", "skipped", "cancelled", "timed_out", "action_required", "startup_failure", "stale"].includes(conclusion)) {
269
+ conclusion = "unknown";
270
+ warn("checks-unknown-conclusion", "Se recibió una conclusión de comprobación desconocida.");
271
+ }
272
+ const name = safeText(rawName, 200, "check.name");
273
+ const url = safeUrl(kind === "check-run" ? value.html_url : value.target_url);
274
+ const rawDate = kind === "check-run" ? value.completed_at ?? value.started_at : value.updated_at;
275
+ const timestamp = typeof rawDate === "string" && !Number.isNaN(Date.parse(rawDate)) ? new Date(rawDate).toISOString() : null;
276
+ if (timestamp === null) warn("checks-date-missing", "Una comprobación no tiene fecha válida disponible.");
277
+ const key = `${kind}:${kind === "check-run" ? value.app.id : "context"}:${rawName}`;
278
+ const next = { kind, name, state, conclusion, url, commit, timestamp };
279
+ const prev = results.get(key);
280
+ if (!prev || Date.parse(timestamp ?? "") >= Date.parse(prev.timestamp ?? "") || !prev.timestamp) results.set(key, next);
281
+ }
282
+ if (count === response.total_count) return;
283
+ if (values.length < perPage || count > response.total_count) break;
284
+ page++;
285
+ }
286
+ throw new SyncInspectionError("checks-limit", "No se pudieron recuperar todas las comprobaciones.");
287
+ }
288
+ for (const kind of ["check-run", "commit-status"]) {
289
+ try { await collect(kind); }
290
+ catch (error) {
291
+ if (!(error instanceof SyncInspectionError)) throw error;
292
+ complete = false;
293
+ warn(`checks-${error.code}`, "Una sección de comprobaciones está incompleta o no disponible.");
294
+ }
295
+ }
296
+ const items = [...results.values()].sort((a, b) => `${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`, "en"));
297
+ const summary = { success: 0, adverse: 0, pending: 0, other: 0 };
298
+ for (const item of items) {
299
+ if (ADVERSE.has(item.state) || ADVERSE.has(item.conclusion)) summary.adverse++;
300
+ else if (item.conclusion === "success" && (item.state === "completed" || item.state === "success")) summary.success++;
301
+ else if (item.state === "pending" || ["queued", "in_progress", "waiting", "requested"].includes(item.state)) summary.pending++;
302
+ else summary.other++;
303
+ }
304
+ if (!items.length) warn("checks-missing", "No hay evidencia disponible de CI para el SHA de cabecera del PR.");
305
+ if (summary.pending || summary.other) warn("checks-not-success", "Hay comprobaciones pendientes, omitidas, neutrales o desconocidas.");
306
+ return { subject: "pr-head", commit, historical: false, complete, pages, received, items, summary };
307
+ }
308
+
309
+ const RULES = [
310
+ { id: "backend-path", category: "backend", test: /(?:^|\/)[^/]*(?:backend|server|api)[^/]*(?:\/|$)/i, destinations: /(?:BACKEND|ARQUITECTURA|IMPLEMENTACION)$/ },
311
+ { id: "frontend-path", category: "frontend", test: /(?:^|\/)[^/]*(?:frontend|client|web|ui)[^/]*(?:\/|$)/i, destinations: /(?:FRONTEND|IMPLEMENTACION)$/ },
312
+ { id: "firmware-path", category: "hardware", test: /(?:^|\/)[^/]*(?:firmware|hardware|iot|esp32)[^/]*(?:\/|$)|\.(?:ino)$/i, destinations: /(?:IOT|HARDWARE|IMPLEMENTACION)$/ },
313
+ { id: "docs-path", category: "documentation", test: /(?:^|\/)(?:docs|documentation)(?:\/|$)|\.(?:md|mdx)$/i, destinations: /(?:PRODUCTO|ARQUITECTURA|IMPLEMENTACION)$/ },
314
+ { id: "operations-path", category: "operations", test: /(?:^|\/)(?:\.github|infra|deploy|docker|scripts)(?:\/|$)|(?:^|\/)(?:Dockerfile|docker-compose[^/]*)$/i, destinations: /(?:OPERACION)$/ },
315
+ { id: "tests-path", category: "tests", test: /(?:^|\/)(?:test|tests|__tests__)(?:\/|$)|\.(?:test|spec)\.[^/]+$/i, destinations: /(?:IMPLEMENTACION|ENTREGAS|SPRINTS)$/ },
316
+ { id: "decisions-path", category: "decisions", test: /(?:^|\/)(?:adr|decisions|decisiones)(?:\/|$)/i, destinations: /(?:DECISIONES)$/ }
317
+ ];
318
+
319
+ function resolveCandidateAreas(vault, layout, files, warn) {
320
+ const dirs = layout?.profile === "declared" ? layout.requiredDirectories : CANONICAL_DIRS;
321
+ const allowed = [];
322
+ for (const dir of dirs ?? []) {
323
+ if (!validRelativePath(dir) || dir.includes("/") || containsSensitiveData(dir) || isCredentialPath(dir)) continue;
324
+ try {
325
+ const stat = fs.lstatSync(path.join(vault, dir));
326
+ if (stat.isDirectory() && !stat.isSymbolicLink()) allowed.push(dir);
327
+ } catch { /* Doctor supplies structural diagnostics. */ }
328
+ }
329
+ const candidateAreas = [];
330
+ const unmappedFiles = [];
331
+ for (const file of files) {
332
+ if (!RULES.some((rule) => [file.path, file.previousPath].filter(Boolean).some((name) => rule.test.test(name)))) unmappedFiles.push(file.path);
333
+ }
334
+ if (unmappedFiles.length) warn("unmapped-files", "Hay archivos sin clasificación; requieren revisión manual.");
335
+ for (const rule of RULES) {
336
+ const matchingFiles = files.filter((file) => [file.path, file.previousPath].filter(Boolean).some((name) => rule.test.test(name))).map((file) => file.path);
337
+ if (!matchingFiles.length) continue;
338
+ const destinations = allowed.filter((dir) => rule.destinations.test(dir.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase()));
339
+ // Prefer a precise area (e.g. BACKEND), otherwise a single generic match.
340
+ const exact = destinations.filter((dir) => dir.toLowerCase().endsWith(rule.category) || rule.category === "hardware" && /(?:iot|hardware)$/i.test(dir));
341
+ const choices = exact.length ? exact : destinations;
342
+ const directory = choices.length === 1 ? choices[0] : null;
343
+ const notes = [];
344
+ let omittedNotes = 0;
345
+ if (directory === null) warn("area-unresolved", "Una categoría no tiene carpeta inequívoca; requiere revisión manual.");
346
+ else {
347
+ try {
348
+ // Direct entries only: never traverse nested Skills/resources or symlinks.
349
+ const entries = fs.readdirSync(path.join(vault, directory), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name, "en"));
350
+ for (const entry of entries) {
351
+ const note = `${directory}/${entry.name}`;
352
+ if (!validRelativePath(note) || containsSensitiveData(note) || isCredentialPath(note)) { omittedNotes++; continue; }
353
+ if (notes.length < INSPECT_LIMITS.notes) notes.push(note); else omittedNotes++;
354
+ }
355
+ if (omittedNotes) warn("notes-omitted", "Se omitieron rutas de notas por seguridad o límite de candidatas.");
356
+ } catch { warn("notes-unavailable", "No se pudieron enumerar las notas de un área candidata."); }
357
+ }
358
+ candidateAreas.push({ category: rule.category, directory, notes, rule: rule.id, files: matchingFiles, omittedNotes });
359
+ }
360
+ if (!candidateAreas.length) warn("no-candidates", "No hay áreas candidatas; requiere revisión manual, no equivale a ausencia de cambios documentales.");
361
+ return { candidateAreas, unmappedFiles };
362
+ }
363
+
364
+ export function formatSyncInspectionReport(report) {
365
+ const lines = [
366
+ `Project Memory Sync Inspect: ${report.status.toUpperCase()}`,
367
+ `Repositorio: ${report.repository}; rama: ${report.branch}`,
368
+ `PR #${report.pullRequest.number}: ${report.pullRequest.title ?? "[omitido]"}`,
369
+ report.pullRequest.url,
370
+ `Integración: ${report.pullRequest.mergeCommit}; fecha: ${report.pullRequest.mergedAt}`,
371
+ `Ya revisado: ${report.alreadyReviewed ? "sí" : "no"}; registro: ${report.ledgerPath}`,
372
+ `Consulta: ${report.observedAt}`,
373
+ "Descripción atribuida al PR (no verificación):", report.pullRequest.description ?? "[sin descripción visible]",
374
+ `Archivos: ${report.coverage.files.visible} visibles / ${report.coverage.files.expected} esperados; ${report.coverage.files.omitted} omitidos; cobertura ${report.coverage.files.complete ? "completa" : "incompleta"}`,
375
+ `Inventario: ${report.coverage.files.received} recibidos, ${report.coverage.files.unique} únicos, ${report.coverage.files.pages} páginas`
376
+ ];
377
+ for (const file of report.files) lines.push(`- ${file.change}: ${file.path}${file.previousPath ? ` (antes: ${file.previousPath})` : ""}; +${file.additions} -${file.deletions}`);
378
+ lines.push(`CI del SHA de cabecera ${report.checks.commit} (no prueba de integración ni ejecución local):`);
379
+ for (const item of report.checks.items) lines.push(`- ${item.kind}: ${item.name ?? "[omitido]"}; ${item.state}/${item.conclusion ?? "sin conclusión"}; ${item.url ?? "sin referencia"}${item.timestamp ? `; ${item.timestamp}` : ""}`);
380
+ lines.push(`CI: ${report.checks.summary.success} éxitos, ${report.checks.summary.adverse} adversos, ${report.checks.summary.pending} pendientes, ${report.checks.summary.other} otros; cobertura ${report.checks.complete ? "completa" : "incompleta"}`);
381
+ lines.push("Áreas candidatas (no autorización para editar):");
382
+ for (const area of report.candidateAreas) {
383
+ lines.push(`- ${area.category}: ${area.directory ?? "sin carpeta inequívoca"}; regla ${area.rule}; ${area.omittedNotes} notas omitidas`);
384
+ for (const file of area.files) lines.push(` motivo: ${file}`);
385
+ for (const note of area.notes) lines.push(` candidata: ${note}`);
386
+ }
387
+ for (const file of report.unmappedFiles) lines.push(`Sin clasificación: ${file}`);
388
+ lines.push(`Límites: ${report.coverage.files.limit} archivos; ${report.coverage.candidateNotesLimit} notas por área; ${report.checks.received} checks recibidos en ${report.checks.pages} páginas`);
389
+ for (const warning of report.warnings) lines.push(`! [${warning.code}] ${warning.message}`);
390
+ lines.push("Inspección de solo lectura. ready no significa aprobación del código ni de la memoria.");
391
+ const text = lines.join("\n");
392
+ if (Buffer.byteLength(text) > INSPECT_LIMITS.outputBytes) throw new SyncInspectionError("output-limit", "El informe supera el límite seguro de salida.");
393
+ return text;
394
+ }
@@ -0,0 +1,94 @@
1
+ import { containsSensitiveData } from "./sensitive-data.js";
2
+
3
+ const HEADERS = [
4
+ ["Cambio", "Estado", "Fecha", "Resultado", "Commit de memoria"],
5
+ ["PR", "Estado", "Fecha de revisión", "Resultado", "Commit de memoria"]
6
+ ];
7
+
8
+ export class LedgerError extends Error {
9
+ constructor() {
10
+ super("El registro contiene una tabla ambigua, incompleta o incompatible.");
11
+ this.name = "LedgerError";
12
+ }
13
+ }
14
+
15
+ // Strict writer parser. The existing tolerant sync-plan reader stays unchanged.
16
+ export function parseWritableLedger(content, repository) {
17
+ const bad = () => { throw new LedgerError(); };
18
+ if (typeof content !== "string" || containsSensitiveData(content)
19
+ || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(content)) bad();
20
+ const lines = [];
21
+ let offset = 0, fence = null, comment = false;
22
+ const start = content.replace(/^\uFEFF/, "");
23
+ let frontmatter = start.startsWith("---\n") || start.startsWith("---\r\n");
24
+ for (const text of content.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
25
+ const raw = text.replace(/\r?\n$/, "");
26
+ let active = !fence && !frontmatter && !comment && !/^(?: {4}|\t)/.test(raw);
27
+ if (frontmatter && offset > 0 && raw.trim() === "---") frontmatter = false;
28
+ const marker = raw.match(/^\s{0,3}(`{3,}|~{3,})/);
29
+ if (marker) {
30
+ active = false;
31
+ if (!fence) fence = marker[1];
32
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = null;
33
+ }
34
+ if (!fence && raw.includes("<!--")) { active = false; comment = true; }
35
+ if (!fence && raw.includes("-->") && comment) { active = false; comment = false; }
36
+ lines.push({ raw, start: offset, end: offset + text.length, active });
37
+ offset += text.length;
38
+ }
39
+ if (frontmatter || fence || comment) bad();
40
+ const row = (raw) => {
41
+ const text = raw.trim();
42
+ return text.startsWith("|") && text.endsWith("|")
43
+ ? text.slice(1, -1).split("|").map((cell) => cell.trim()) : null;
44
+ };
45
+ const headers = lines.flatMap((line, index) => {
46
+ const cells = line.active && row(line.raw);
47
+ return cells && HEADERS.some((h) => h.every((v, i) => cells[i] === v)) ? [index] : [];
48
+ });
49
+ if (headers.length !== 1) bad();
50
+ const headerIndex = headers[0];
51
+ const separator = row(lines[headerIndex + 1]?.raw ?? "");
52
+ if (row(lines[headerIndex].raw).length !== 5 || !separator || separator.length !== 5
53
+ || !separator.every((cell) => /^:?-{3,}:?$/.test(cell)) || !lines[headerIndex + 1].active) bad();
54
+ const records = new Map();
55
+ let insertion = lines[headerIndex + 1].end;
56
+ for (const line of lines.slice(headerIndex + 2)) {
57
+ if (!line.active) break;
58
+ if (!line.raw.trim()) continue;
59
+ if (!line.raw.trim().startsWith("|")) break;
60
+ const cells = row(line.raw);
61
+ if (!cells || cells.length !== 5 || cells.some((cell) => !cell)) bad();
62
+ const link = cells[0].match(/^\[[^\]\r\n]+\]\((https:\/\/github\.com\/[^\s)]+)\)$/i);
63
+ if (!link) bad();
64
+ let url;
65
+ try { url = new URL(link[1]); } catch { bad(); }
66
+ const suffix = url.pathname.match(/\/pull\/([1-9]\d*)$/);
67
+ const number = Number(suffix?.[1]);
68
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password
69
+ || url.search || url.hash || !Number.isSafeInteger(number)
70
+ || url.pathname.toLowerCase() !== `/${repository}/pull/${number}`.toLowerCase()
71
+ || records.has(number) || !["Revisado", "Pendiente"].includes(cells[1])
72
+ || !validDate(cells[2])) bad();
73
+ records.set(number, { number, state: cells[1], date: cells[2], result: cells[3], commit: cells[4] });
74
+ insertion = line.end;
75
+ }
76
+ const endings = content.match(/\r?\n/g) ?? [];
77
+ const newline = endings[0] ?? "\n";
78
+ if (endings.some((value) => value !== newline) || /\r(?!\n)/.test(content)) bad();
79
+ return { content, records, insertion, newline };
80
+ }
81
+
82
+ export function appendLedgerRecord(ledger, { repository, pr, date, result }) {
83
+ const { content, insertion, newline } = ledger;
84
+ const prefix = content.slice(0, insertion);
85
+ const boundary = prefix.endsWith("\n") ? "" : newline;
86
+ const row = `| [#${pr}](https://github.com/${repository}/pull/${pr}) | Revisado | ${date} | ${result} | — |`;
87
+ return `${prefix}${boundary}${row}${newline}${content.slice(insertion)}`;
88
+ }
89
+
90
+ function validDate(value) {
91
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
92
+ const date = new Date(`${value}T00:00:00Z`);
93
+ return !Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === value;
94
+ }
@@ -0,0 +1,151 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { containsSensitiveData, isCredentialPath } from "./sensitive-data.js";
4
+ import { APPLY_LIMITS, portablePathKey } from "./sync-proposal.js";
5
+ import { memoryPath, readSyncFile, rejectFile } from "./sync-files.js";
6
+
7
+ export function collectMemoryNotes(vault) {
8
+ const notes = new Map(), pending = [vault]; let entries = 0, bytes = 0;
9
+ while (pending.length) {
10
+ const dir = pending.pop();
11
+ // opendir keeps a hostile oversized directory bounded before materializing it.
12
+ const handle = fs.opendirSync(dir);
13
+ try {
14
+ let entry;
15
+ while ((entry = handle.readSync())) {
16
+ if (++entries > APPLY_LIMITS.entries) rejectFile("virtual-limit", "El recorrido de la memoria supera el límite.", "blocked");
17
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
18
+ const full = path.join(dir, entry.name);
19
+ if (entry.isSymbolicLink()) { if (/\.md$/i.test(entry.name)) rejectFile("unsafe-path", "La vista de memoria contiene una nota enlazada."); continue; }
20
+ if (entry.isDirectory()) { pending.push(memoryPath(vault, path.relative(vault, full))); continue; }
21
+ if (!/\.md$/i.test(entry.name)) continue;
22
+ if (notes.size >= APPLY_LIMITS.virtualNotes) rejectFile("virtual-limit", "Hay demasiadas notas para validar la vista completa.", "blocked");
23
+ const file = readSyncFile(memoryPath(vault, path.relative(vault, full)), APPLY_LIMITS.note);
24
+ bytes += file.bytes.length;
25
+ if (bytes > APPLY_LIMITS.virtualBytes) rejectFile("virtual-limit", "La vista de memoria supera el presupuesto de texto.", "blocked");
26
+ notes.set(path.relative(vault, full).split(path.sep).join("/"), file);
27
+ }
28
+ } finally { handle.closeSync(); }
29
+ }
30
+ const names = new Set();
31
+ for (const name of notes.keys()) {
32
+ const key = portablePathKey(name);
33
+ if (names.has(key)) rejectFile("ambiguous-path", "La memoria contiene rutas Markdown ambiguas entre plataformas.");
34
+ names.add(key);
35
+ }
36
+ return new Map([...notes].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
37
+ }
38
+
39
+ export function validateNoteText(text, original) {
40
+ if (containsSensitiveData(text)) rejectFile("sensitive-note", "Una nota contiene patrones sensibles.", "blocked");
41
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(text)
42
+ || !text.endsWith("\n") || /[ \t]+\r?$/m.test(text) || /\{\{[^{}]+\}\}/.test(text)) rejectFile("note-format", "Una nota contiene controles, marcadores o errores de formato.", "blocked");
43
+ const endings = text.match(/\r?\n/g) ?? [], ending = endings[0];
44
+ if (endings.some((item) => item !== ending) || /\r(?!\n)/.test(text)) rejectFile("note-format", "Una nota mezcla estilos de salto de línea.", "blocked");
45
+ if (original !== undefined && (text.startsWith("\uFEFF") !== original.startsWith("\uFEFF") || ending !== original.match(/\r?\n/)?.[0])) rejectFile("note-style", "La propuesta cambia el BOM o el estilo de salto de línea.", "blocked");
46
+ }
47
+
48
+ export function validateVirtualMemory(vault, catalog, replacements) {
49
+ const virtual = new Map([...catalog].map(([name, file]) => [name, replacements.get(name) ?? file.text]));
50
+ let total = 0;
51
+ for (const text of virtual.values()) {
52
+ total += Buffer.byteLength(text);
53
+ if (total > APPLY_LIMITS.virtualBytes) rejectFile("virtual-limit", "La memoria resultante supera el presupuesto de texto.", "blocked");
54
+ if (containsSensitiveData(text) || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(text)) rejectFile("unsafe-memory", "La vista resultante contiene patrones sensibles o controles.", "blocked");
55
+ }
56
+ const byBase = new Map(), byPath = new Map(), bodies = new Map(), headings = new Map(), attachments = new Set();
57
+ for (const [name, text] of virtual) {
58
+ byPath.set(portablePathKey(name), name);
59
+ const base = portablePathKey(path.posix.basename(name).replace(/\.md$/i, ""));
60
+ byBase.set(base, [...(byBase.get(base) ?? []), name]);
61
+ const body = markdownBody(text); bodies.set(name, body.replace(/(`+)[^`\n]*?\1/g, (token) => " ".repeat(token.length)));
62
+ headings.set(name, [...body.matchAll(/^ {0,3}#{1,6} (.+)$/gm)].map((m) => m[1].replace(/[ \t]+#+[ \t]*$/, "").trim())
63
+ .filter((title) => !/[`*\[\]<>]/.test(title) && !/_[^_]+_/.test(title)));
64
+ }
65
+ const fragment = (name, value, wiki) => {
66
+ if (!value || !headings.has(name)) rejectFile("unsupported-fragment", "Un fragmento local no se puede resolver con el subconjunto soportado.", "blocked");
67
+ const normalize = wiki ? (s) => s.trim().normalize("NFC") : slug;
68
+ const target = wiki ? value.trim().normalize("NFC") : decode(value);
69
+ if (target.startsWith("^") || (headings.get(name) ?? []).filter((title) => normalize(title) === target).length !== 1) rejectFile("note-links", "Un enlace de sección es inexistente, ambiguo o no soportado.", "blocked");
70
+ };
71
+ const markdownTarget = (from, raw) => {
72
+ if (/^(?:https?:|mailto:|data:|obsidian:)/i.test(raw)) return;
73
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) rejectFile("note-links", "Un enlace usa un protocolo no soportado.", "blocked");
74
+ const hash = raw.indexOf("#"), destination = hash < 0 ? raw : raw.slice(0, hash);
75
+ const decoded = decode(destination.split("?")[0]);
76
+ const relative = decoded ? path.posix.normalize(path.posix.join(path.posix.dirname(from), decoded)) : from;
77
+ if (decoded.startsWith("/") || decoded.includes("\\") || isCredentialPath(relative) || relative.split("/").some((p) => p.startsWith("."))) rejectFile("note-links", "Un enlace local sale de las áreas consultables.", "blocked");
78
+ const canonical = byPath.get(portablePathKey(relative));
79
+ if (/\.md$/i.test(relative) && !canonical) rejectFile("note-links", "Un enlace apunta a una nota ausente.", "blocked");
80
+ if (!canonical) {
81
+ let stat;
82
+ try { stat = fs.lstatSync(memoryPath(vault, relative)); }
83
+ catch { rejectFile("note-links", "Un enlace local no tiene un destino físico seguro.", "blocked"); }
84
+ if (!stat.isFile()) rejectFile("note-links", "Un enlace local no apunta a un archivo regular.", "blocked");
85
+ attachments.add(relative);
86
+ }
87
+ if (hash >= 0) fragment(canonical ?? relative, raw.slice(hash + 1), false);
88
+ };
89
+ for (const [from, body] of bodies) {
90
+ for (const match of body.matchAll(/!?\[\[([^\]]+)\]\]/g)) {
91
+ const [target] = match[1].split("|"); const hash = target.indexOf("#");
92
+ const note = (hash < 0 ? target : target.slice(0, hash)).trim().replace(/\.md$/i, "");
93
+ let destination;
94
+ if (!note) destination = from;
95
+ else if (note.includes("/")) destination = byPath.get(portablePathKey(`${note}.md`));
96
+ else { const candidates = byBase.get(portablePathKey(note)) ?? []; if (candidates.length === 1) destination = candidates[0]; }
97
+ if (!destination) rejectFile("note-links", "Un enlace Wiki es inexistente o ambiguo.", "blocked");
98
+ memoryPath(vault, destination);
99
+ if (hash >= 0) fragment(destination, target.slice(hash + 1), true);
100
+ }
101
+ const clean = body.replace(/!?\[\[[^\]]+\]\]/g, "");
102
+ for (const match of clean.matchAll(/!?\[[^\]\n]*\]\(([^\n]+?)\)/g)) {
103
+ const parsed = match[1].match(/^(?:<([^<>]+)>|([^\s]+))(?:\s+"[^"\n]*")?$/);
104
+ if (!parsed || /[()\\]/.test(parsed[1] ?? parsed[2])) rejectFile("unsupported-link", "La sintaxis de un enlace local no está soportada.", "blocked");
105
+ markdownTarget(from, parsed[1] ?? parsed[2]);
106
+ }
107
+ // Reference-style links need their own syntax contract; fail closed rather
108
+ // than silently certifying an unchecked target or fragment.
109
+ if (/^ {0,3}\[[^\]]+\]:|!?\[[^\]\n]+\]\[[^\]\n]*\]/m.test(clean)) rejectFile("unsupported-link", "Los enlaces por referencia requieren una revisión fuera de este subconjunto.", "blocked");
110
+ }
111
+ return [...attachments].sort();
112
+ }
113
+ function decode(value) {
114
+ try { return decodeURIComponent(value); } catch { rejectFile("note-links", "Un enlace tiene codificación inválida.", "blocked"); }
115
+ }
116
+ function slug(value) { return value.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-"); }
117
+ function markdownBody(text) {
118
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/); let fence, comment = false, front = lines[0] === "---";
119
+ const body = [];
120
+ for (let i = 0; i < lines.length; i++) {
121
+ let line = lines[i];
122
+ if (front) { if (i && line.trim() === "---") front = false; body.push(""); continue; }
123
+ if (comment) {
124
+ const end = line.indexOf("-->");
125
+ if (end < 0) { body.push(""); continue; }
126
+ line = line.slice(end + 3); comment = false;
127
+ }
128
+ if (!fence) {
129
+ line = line.replace(/<!--.*?-->/g, "");
130
+ const start = line.indexOf("<!--");
131
+ if (start >= 0) { comment = true; line = line.slice(0, start); }
132
+ }
133
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
134
+ if (marker) { if (!fence) fence = marker[1]; else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = undefined; body.push(""); continue; }
135
+ body.push(fence || /^(?: {4}|\t)/.test(line) ? "" : line);
136
+ }
137
+ if (front || fence || comment) rejectFile("unsupported-markdown", "Una nota contiene frontmatter, código o comentario sin cierre.", "blocked");
138
+ return body.join("\n");
139
+ }
140
+
141
+ export function noteUnifiedDiff(name, before, after) {
142
+ const left = before.split(/\r?\n/).slice(0, -1), right = after.split(/\r?\n/).slice(0, -1);
143
+ let start = 0, tail = 0;
144
+ while (start < Math.min(left.length, right.length) && left[start] === right[start]) start++;
145
+ while (tail < Math.min(left.length, right.length) - start && left[left.length - 1 - tail] === right[right.length - 1 - tail]) tail++;
146
+ const prefix = Math.max(0, start - 3), suffix = Math.min(tail, 3);
147
+ const lines = [`--- a/${name}`, `+++ b/${name}`, `@@ -${prefix + 1},${left.length - tail + suffix - prefix} +${prefix + 1},${right.length - tail + suffix - prefix} @@`,
148
+ ...left.slice(prefix, start).map((s) => ` ${s}`), ...left.slice(start, left.length - tail).map((s) => `-${s}`),
149
+ ...right.slice(start, right.length - tail).map((s) => `+${s}`), ...left.slice(left.length - tail, left.length - tail + suffix).map((s) => ` ${s}`)];
150
+ return lines.join("\n") + "\n";
151
+ }