sinapse-ai 9.4.0 → 9.5.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 (266) hide show
  1. package/.claude/CLAUDE.md +10 -4
  2. package/.claude/hooks/enforce-architecture-first.py +197 -197
  3. package/.claude/hooks/enforce-git-push-authority.sh +25 -4
  4. package/.claude/hooks/mind-clone-governance.py +193 -193
  5. package/.claude/hooks/read-protection.py +152 -152
  6. package/.claude/hooks/sql-governance.py +183 -183
  7. package/.claude/hooks/verify-packages.cjs +83 -0
  8. package/.claude/hooks/write-path-validation.py +195 -195
  9. package/.claude/rules/hook-governance.md +1 -0
  10. package/.claude/rules/mandatory-delegation.md +24 -0
  11. package/.claude/rules/project-intelligence.md +63 -0
  12. package/.claude/rules/response-format.md +4 -0
  13. package/.claude/rules/safe-collaboration.md +4 -2
  14. package/.claude/rules/security-data-protection.md +18 -0
  15. package/.claude/rules/squad-awareness.md +93 -67
  16. package/.claude/rules/token-economy.md +148 -0
  17. package/.codex/agents/analyst.md +90 -0
  18. package/.codex/agents/architect.md +78 -0
  19. package/.codex/agents/data-engineer.md +38 -0
  20. package/.codex/agents/developer.md +97 -0
  21. package/.codex/agents/devops.md +121 -0
  22. package/.codex/agents/product-lead.md +27 -0
  23. package/.codex/agents/project-lead.md +28 -0
  24. package/.codex/agents/quality-gate.md +89 -0
  25. package/.codex/agents/sprint-lead.md +28 -0
  26. package/.codex/agents/squad-creator.md +58 -0
  27. package/.codex/agents/ux-design-expert.md +28 -0
  28. package/.sinapse-ai/core/code-intel/registry-syncer.js +56 -3
  29. package/.sinapse-ai/core/doctor/checks/agent-memory.js +5 -1
  30. package/.sinapse-ai/core/doctor/checks/claude-md.js +4 -1
  31. package/.sinapse-ai/core/doctor/checks/code-intel.js +5 -1
  32. package/.sinapse-ai/core/doctor/checks/commands-count.js +4 -1
  33. package/.sinapse-ai/core/doctor/checks/constitution-consistency.js +4 -1
  34. package/.sinapse-ai/core/doctor/checks/core-config.js +4 -1
  35. package/.sinapse-ai/core/doctor/checks/entity-registry.js +6 -1
  36. package/.sinapse-ai/core/doctor/checks/git-hooks.js +5 -1
  37. package/.sinapse-ai/core/doctor/checks/graph-dashboard.js +4 -1
  38. package/.sinapse-ai/core/doctor/checks/hooks-claude-count.js +5 -1
  39. package/.sinapse-ai/core/doctor/checks/ide-sync.js +4 -1
  40. package/.sinapse-ai/core/doctor/checks/node-version.js +4 -1
  41. package/.sinapse-ai/core/doctor/checks/npm-packages.js +4 -1
  42. package/.sinapse-ai/core/doctor/checks/rules-files.js +4 -1
  43. package/.sinapse-ai/core/doctor/checks/settings-json.js +4 -1
  44. package/.sinapse-ai/core/doctor/checks/skills-count.js +4 -1
  45. package/.sinapse-ai/core/doctor/index.js +157 -50
  46. package/.sinapse-ai/core/ids/registry-updater.js +6 -1
  47. package/.sinapse-ai/core/logger/index.js +319 -0
  48. package/.sinapse-ai/core/orchestration/terminal-spawner.js +2 -2
  49. package/.sinapse-ai/core/telemetry/index.js +247 -0
  50. package/.sinapse-ai/data/entity-registry.yaml +1384 -944
  51. package/.sinapse-ai/development/agents/architect.md +5 -0
  52. package/.sinapse-ai/development/agents/data-engineer.md +38 -0
  53. package/.sinapse-ai/development/agents/developer.md +28 -0
  54. package/.sinapse-ai/development/agents/devops.md +4 -0
  55. package/.sinapse-ai/development/agents/product-lead.md +27 -0
  56. package/.sinapse-ai/development/agents/project-lead.md +28 -0
  57. package/.sinapse-ai/development/agents/quality-gate.md +4 -0
  58. package/.sinapse-ai/development/agents/sprint-lead/MEMORY.md +8 -0
  59. package/.sinapse-ai/development/agents/sprint-lead.md +28 -0
  60. package/.sinapse-ai/development/agents/squad-creator.md +58 -0
  61. package/.sinapse-ai/development/agents/ux-design-expert.md +28 -0
  62. package/.sinapse-ai/development/knowledge-base/agent-communication-protocol.md +127 -0
  63. package/.sinapse-ai/development/knowledge-base/database-scaling-patterns.md +374 -0
  64. package/.sinapse-ai/development/knowledge-base/environment-deployment-patterns.md +353 -0
  65. package/.sinapse-ai/development/knowledge-base/gotchas-patterns.md +224 -0
  66. package/.sinapse-ai/development/knowledge-base/infrastructure-decision-framework.md +221 -0
  67. package/.sinapse-ai/development/knowledge-base/security-pre-deploy-checklist.md +410 -0
  68. package/.sinapse-ai/development/knowledge-base/software-architecture-patterns.md +299 -0
  69. package/.sinapse-ai/development/knowledge-base/token-economy-guide.md +198 -0
  70. package/.sinapse-ai/development/scripts/populate-entity-registry.js +5 -1
  71. package/.sinapse-ai/development/skills/captcha-handler.md +82 -0
  72. package/.sinapse-ai/development/skills/chrome-brain.md +81 -0
  73. package/.sinapse-ai/development/skills/deploy-readiness.md +93 -0
  74. package/.sinapse-ai/development/skills/model-router.md +92 -0
  75. package/.sinapse-ai/development/skills/sinapse-methodology.md +175 -0
  76. package/.sinapse-ai/development/skills/story-fast-track.md +71 -0
  77. package/.sinapse-ai/development/tasks/dev-develop-story.md +10 -0
  78. package/.sinapse-ai/development/tasks/environment-promotion-pipeline.md +582 -0
  79. package/.sinapse-ai/development/tasks/generate-agent-handoff.md +223 -0
  80. package/.sinapse-ai/development/tasks/infrastructure-assessment.md +432 -0
  81. package/.sinapse-ai/development/tasks/load-testing-setup.md +611 -0
  82. package/.sinapse-ai/development/tasks/observability-blueprint.md +562 -0
  83. package/.sinapse-ai/development/templates/legal/breach-notification-tmpl.md +113 -0
  84. package/.sinapse-ai/development/templates/legal/privacy-policy-tmpl.md +93 -0
  85. package/.sinapse-ai/development/templates/legal/terms-of-service-tmpl.md +85 -0
  86. package/.sinapse-ai/development/templates/service-template/README.md.hbs +159 -159
  87. package/.sinapse-ai/development/templates/service-template/__tests__/index.test.ts.hbs +238 -238
  88. package/.sinapse-ai/development/templates/service-template/client.ts.hbs +404 -404
  89. package/.sinapse-ai/development/templates/service-template/errors.ts.hbs +183 -183
  90. package/.sinapse-ai/development/templates/service-template/index.ts.hbs +121 -121
  91. package/.sinapse-ai/development/templates/service-template/package.json.hbs +88 -88
  92. package/.sinapse-ai/development/templates/service-template/types.ts.hbs +146 -146
  93. package/.sinapse-ai/development/templates/squad-template/LICENSE +22 -22
  94. package/.sinapse-ai/development/workflows/story-development-cycle.yaml +40 -1
  95. package/.sinapse-ai/hooks/ids-post-commit.js +22 -0
  96. package/.sinapse-ai/infrastructure/contracts/compatibility/README.md +42 -0
  97. package/.sinapse-ai/infrastructure/contracts/compatibility/sinapse-current.yaml +35 -0
  98. package/.sinapse-ai/infrastructure/scripts/llm-routing/templates/claude-free-tracked.cmd +127 -127
  99. package/.sinapse-ai/infrastructure/scripts/llm-routing/templates/deepseek-proxy.cmd +71 -71
  100. package/.sinapse-ai/infrastructure/scripts/llm-routing/templates/deepseek-usage.cmd +51 -51
  101. package/.sinapse-ai/infrastructure/scripts/pr-review-ai.js +16 -13
  102. package/.sinapse-ai/infrastructure/scripts/setup-project-infra.js +128 -0
  103. package/.sinapse-ai/infrastructure/scripts/test-discovery.js +8 -3
  104. package/.sinapse-ai/infrastructure/scripts/validate-manifest-parity.js +380 -0
  105. package/.sinapse-ai/infrastructure/scripts/validate-parity.js +76 -25
  106. package/.sinapse-ai/infrastructure/templates/coderabbit.yaml.template +280 -280
  107. package/.sinapse-ai/infrastructure/templates/config/env.example +16 -0
  108. package/.sinapse-ai/infrastructure/templates/config/gitignore-additions.tmpl +59 -0
  109. package/.sinapse-ai/infrastructure/templates/github/CODEOWNERS.template +12 -0
  110. package/.sinapse-ai/infrastructure/templates/github/PULL_REQUEST_TEMPLATE.md +29 -0
  111. package/.sinapse-ai/infrastructure/templates/github/ci-template.yml +77 -0
  112. package/.sinapse-ai/infrastructure/templates/github/issue-templates/bug_report.md +34 -0
  113. package/.sinapse-ai/infrastructure/templates/github/issue-templates/feature_request.md +19 -0
  114. package/.sinapse-ai/infrastructure/templates/github-workflows/ci.yml.template +170 -170
  115. package/.sinapse-ai/infrastructure/templates/github-workflows/pr-automation.yml.template +331 -331
  116. package/.sinapse-ai/infrastructure/templates/github-workflows/release.yml.template +197 -197
  117. package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +19 -19
  118. package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-node.tmpl +86 -86
  119. package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-python.tmpl +146 -146
  120. package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-sinapse-base.tmpl +64 -64
  121. package/.sinapse-ai/infrastructure/templates/sinapse-sync.yaml.template +183 -183
  122. package/.sinapse-ai/install-manifest.yaml +275 -140
  123. package/.sinapse-ai/local-config.yaml.template +65 -65
  124. package/.sinapse-ai/monitor/hooks/lib/__init__.py +2 -2
  125. package/.sinapse-ai/monitor/hooks/lib/enrich.py +59 -59
  126. package/.sinapse-ai/monitor/hooks/lib/send_event.py +48 -48
  127. package/.sinapse-ai/monitor/hooks/notification.py +30 -30
  128. package/.sinapse-ai/monitor/hooks/post_tool_use.py +46 -46
  129. package/.sinapse-ai/monitor/hooks/pre_compact.py +30 -30
  130. package/.sinapse-ai/monitor/hooks/pre_tool_use.py +41 -41
  131. package/.sinapse-ai/monitor/hooks/stop.py +30 -30
  132. package/.sinapse-ai/monitor/hooks/subagent_stop.py +30 -30
  133. package/.sinapse-ai/monitor/hooks/user_prompt_submit.py +39 -39
  134. package/.sinapse-ai/product/templates/adr.hbs +126 -126
  135. package/.sinapse-ai/product/templates/dbdr.hbs +242 -242
  136. package/.sinapse-ai/product/templates/epic.hbs +213 -213
  137. package/.sinapse-ai/product/templates/pmdr.hbs +187 -187
  138. package/.sinapse-ai/product/templates/prd-v2.0.hbs +217 -217
  139. package/.sinapse-ai/product/templates/prd.hbs +202 -202
  140. package/.sinapse-ai/product/templates/story-tmpl.yaml +59 -0
  141. package/.sinapse-ai/product/templates/story.hbs +264 -264
  142. package/.sinapse-ai/product/templates/task.hbs +171 -171
  143. package/.sinapse-ai/product/templates/tmpl-comment-on-examples.sql +159 -159
  144. package/.sinapse-ai/product/templates/tmpl-migration-script.sql +92 -92
  145. package/.sinapse-ai/product/templates/tmpl-rls-granular-policies.sql +105 -105
  146. package/.sinapse-ai/product/templates/tmpl-rls-kiss-policy.sql +11 -11
  147. package/.sinapse-ai/product/templates/tmpl-rls-roles.sql +136 -136
  148. package/.sinapse-ai/product/templates/tmpl-rls-simple.sql +78 -78
  149. package/.sinapse-ai/product/templates/tmpl-rls-tenant.sql +153 -153
  150. package/.sinapse-ai/product/templates/tmpl-rollback-script.sql +78 -78
  151. package/.sinapse-ai/product/templates/tmpl-seed-data.sql +141 -141
  152. package/.sinapse-ai/product/templates/tmpl-smoke-test.sql +17 -17
  153. package/.sinapse-ai/product/templates/tmpl-staging-copy-merge.sql +140 -140
  154. package/.sinapse-ai/product/templates/tmpl-stored-proc.sql +141 -141
  155. package/.sinapse-ai/product/templates/tmpl-trigger.sql +153 -153
  156. package/.sinapse-ai/product/templates/tmpl-view-materialized.sql +134 -134
  157. package/.sinapse-ai/product/templates/tmpl-view.sql +178 -178
  158. package/.sinapse-ai/scripts/diagnostics/health-dashboard/package-lock.json +427 -355
  159. package/LICENSE +34 -34
  160. package/README.en.md +167 -20
  161. package/README.md +190 -22
  162. package/bin/cli.js +510 -196
  163. package/bin/postinstall.js +564 -0
  164. package/bin/sinapse-cli +283 -283
  165. package/bin/sinapse-graph.js +9 -0
  166. package/bin/sinapse-init.js +36 -4
  167. package/bin/sinapse-minimal.js +20 -9
  168. package/bin/sinapse.js +202 -122
  169. package/bin/utils/deprecation-warning.js +46 -0
  170. package/bin/utils/pre-push-safety.js +14 -0
  171. package/docs/TELEMETRY.md +131 -0
  172. package/docs/chrome-brain-upgrade-plan.md +624 -0
  173. package/docs/framework/orqx-plan.md +1 -1
  174. package/docs/installation/chrome-brain.md +17 -7
  175. package/docs/mega-upgrade-orchestration-plan.md +71 -0
  176. package/docs/pt/contributing.md +20 -0
  177. package/docs/research-synthesis-for-upgrade.md +511 -0
  178. package/docs/security-audit-report.md +306 -0
  179. package/package.json +20 -8
  180. package/packages/installer/src/config/configure-environment.js +19 -44
  181. package/packages/installer/src/detection/detect-project-type.js +181 -63
  182. package/packages/installer/src/installer/manifest-signature.js +32 -17
  183. package/packages/installer/src/wizard/i18n.js +12 -0
  184. package/packages/installer/src/wizard/ide-config-generator.js +8 -39
  185. package/packages/installer/src/wizard/index.js +119 -14
  186. package/packages/installer/src/wizard/questions.js +2 -3
  187. package/packages/installer/tests/integration/environment-configuration.test.js +7 -5
  188. package/packages/installer/tests/unit/detection/detect-project-type.test.js +138 -1
  189. package/packages/installer/tests/unit/doctor/doctor-orchestrator.test.js +3 -3
  190. package/packages/sinapse-install/bin/edmcp.js +0 -0
  191. package/packages/sinapse-install/bin/sinapse-install.js +0 -0
  192. package/packages/sinapse-pro-cli/bin/sinapse-pro.js +0 -0
  193. package/scripts/check-markdown-links.py +353 -353
  194. package/scripts/coverage-report-summary.js +169 -0
  195. package/scripts/generate-install-manifest.js +6 -2
  196. package/scripts/release-readiness.js +169 -0
  197. package/scripts/test-install-matrix-local.sh +153 -0
  198. package/scripts/validate-install-docs.js +394 -0
  199. package/scripts/validate-no-external-refs.js +376 -0
  200. package/scripts/validate-squad-orqx.js +302 -0
  201. package/scripts/validate-story-meta.js +263 -0
  202. package/squads/claude-code-mastery/CHANGELOG.md +1 -1
  203. package/squads/claude-code-mastery/README.md +2 -2
  204. package/squads/claude-code-mastery/squad.yaml +1 -1
  205. package/squads/squad-artdir/README.md +90 -0
  206. package/squads/squad-artdir/agents/accessibility-guardian.md +184 -0
  207. package/squads/squad-artdir/agents/artdir-orqx.md +145 -0
  208. package/squads/squad-artdir/agents/color-psychologist.md +166 -0
  209. package/squads/squad-artdir/agents/cro-persuasion.md +161 -0
  210. package/squads/squad-artdir/agents/design-system-architect.md +100 -0
  211. package/squads/squad-artdir/agents/ia-architect.md +169 -0
  212. package/squads/squad-artdir/agents/interaction-designer.md +162 -0
  213. package/squads/squad-artdir/agents/layout-engineer.md +163 -0
  214. package/squads/squad-artdir/agents/motion-architect.md +185 -0
  215. package/squads/squad-artdir/agents/platform-aesthetic-director.md +84 -0
  216. package/squads/squad-artdir/agents/premium-packaging-strategist.md +107 -0
  217. package/squads/squad-artdir/agents/product-surface-director.md +86 -0
  218. package/squads/squad-artdir/agents/type-systemist.md +138 -0
  219. package/squads/squad-artdir/agents/visual-strategist.md +127 -0
  220. package/squads/squad-artdir/checklists/seven-pillars-validation-checklist.md +172 -0
  221. package/squads/squad-artdir/knowledge-base/case-nyo-ia-reference.md +289 -0
  222. package/squads/squad-artdir/knowledge-base/deliverables-templates.md +457 -0
  223. package/squads/squad-artdir/knowledge-base/motion-technique-catalog.md +247 -0
  224. package/squads/squad-artdir/knowledge-base/premium-packaging-principles.md +133 -0
  225. package/squads/squad-artdir/knowledge-base/psychological-toolkit.md +229 -0
  226. package/squads/squad-artdir/knowledge-base/saas-art-direction-canon.md +242 -0
  227. package/squads/squad-artdir/knowledge-base/seven-pillars-framework.md +289 -0
  228. package/squads/squad-artdir/knowledge-base/ten-pillars-framework.md +221 -0
  229. package/squads/squad-artdir/package.json +20 -0
  230. package/squads/squad-artdir/squad.yaml +271 -0
  231. package/squads/squad-artdir/tasks/audit-conversion.md +97 -0
  232. package/squads/squad-artdir/tasks/audit-drift-multi-surface.md +55 -0
  233. package/squads/squad-artdir/tasks/consult-saas-canon.md +54 -0
  234. package/squads/squad-artdir/tasks/create-art-direction-brief.md +110 -0
  235. package/squads/squad-artdir/tasks/create-premium-packaging-brief.md +61 -0
  236. package/squads/squad-artdir/tasks/create-wireflow.md +84 -0
  237. package/squads/squad-artdir/tasks/design-color-system.md +81 -0
  238. package/squads/squad-artdir/tasks/design-product-surface.md +60 -0
  239. package/squads/squad-artdir/tasks/design-token-system.md +58 -0
  240. package/squads/squad-artdir/tasks/diagnose-visual-language.md +92 -0
  241. package/squads/squad-artdir/tasks/first-5-minutes-choreography.md +65 -0
  242. package/squads/squad-artdir/tasks/specify-motion-system.md +84 -0
  243. package/squads/squad-artdir/tasks/validate-against-pillars.md +143 -0
  244. package/squads/squad-artdir/templates/art-direction-brief-template.md +215 -0
  245. package/squads/squad-artdir/workflows/conversion-audit-cycle.yaml +78 -0
  246. package/squads/squad-artdir/workflows/full-art-direction-cycle.yaml +98 -0
  247. package/squads/squad-artdir/workflows/saas-platform-art-direction-cycle.yaml +174 -0
  248. package/squads/squad-brand/knowledge-base/ai-visual-generation-canon.md +234 -0
  249. package/squads/squad-brand/squad.yaml +20 -6
  250. package/squads/squad-claude/knowledge-base/context-window-optimization.md +1 -1
  251. package/squads/squad-claude/knowledge-base/swarm-orchestration-patterns.md +2 -2
  252. package/squads/squad-content/knowledge-base/ai-native-content-loop.md +220 -0
  253. package/squads/squad-content/knowledge-base/signal-intelligence-v2.md +234 -0
  254. package/squads/squad-content/knowledge-base/task-ownership-map.md +235 -0
  255. package/squads/squad-content/squad.yaml +187 -27
  256. package/squads/squad-copy/knowledge-base/ai-copy-human-loop-canon.md +235 -0
  257. package/squads/squad-copy/squad.yaml +19 -4
  258. package/squads/squad-design/knowledge-base/cross-surface-token-canon.md +209 -0
  259. package/squads/squad-design/squad.yaml +19 -4
  260. package/.sinapse-ai/core/registry/service-registry.json +0 -6346
  261. package/.sinapse-ai/data/registry-update-log.jsonl +0 -1323
  262. package/.sinapse-ai/manifests/agents.csv +0 -29
  263. package/.sinapse-ai/manifests/tasks.csv +0 -204
  264. package/.sinapse-ai/manifests/workers.csv +0 -196
  265. package/squads/squad-growth/tasks/calculate-sample-size.md +0 -121
  266. package/squads/squad-paidmedia/tasks/calculate-sample-size.md +0 -57
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * validate-no-external-refs.js
5
+ *
6
+ * Story 10.17 — Authorial Hygiene & Competitive Reference Guard.
7
+ *
8
+ * Scans the repository for any reference to a competing framework name and
9
+ * fails (exit 1) if any match is found outside an explicit allow-list.
10
+ *
11
+ * The guard keeps the SINAPSE codebase in a 100% authorial voice: the only
12
+ * places in the entire repo where competitive names may legally appear are
13
+ * the MIT `LICENSE` file (legal attribution requirement), the historical
14
+ * process document `docs/research-synthesis-for-upgrade.md`, and a small set
15
+ * of pre-existing upstream-attribution files inherited from the BMAD fork
16
+ * lineage (flagged for future per-file review — see follow-up backlog in
17
+ * `.sinapse-ai/internal/aiox-feature-map.md`).
18
+ *
19
+ * === Source of truth: `git ls-files` (QA v1.1 fix) ===
20
+ *
21
+ * The original implementation parsed `.gitignore` with a custom (naive)
22
+ * matcher. That matcher was too aggressive: because artifact-ignore rules
23
+ * (e.g. `.test.cache/`, `coverage/`) happened to match prefixes that also
24
+ * appeared inside legitimate tracked directories, entire trees like
25
+ * `squads/`, `tests/`, and `scripts/` were incorrectly pruned from the scan.
26
+ * Out of 4493 files tracked by git, the scanner only visited ~1623 (36 %)
27
+ * — which means it was silently blind to the real repo state and missed
28
+ * pre-existing upstream references that QA later caught by hand.
29
+ *
30
+ * The fix replaces the custom walker with a single call to `git ls-files`,
31
+ * which is the exact set of files that will be published to GitHub / npm.
32
+ * This is correct for three reasons:
33
+ *
34
+ * 1. Publishability — `git ls-files` is EXACTLY what ships. Any file we
35
+ * miss there cannot reach a user, so scanning it is meaningless.
36
+ * 2. Zero parser bugs — git does gitignore resolution natively; we don't
37
+ * need to approximate it.
38
+ * 3. Deterministic — no filesystem walk, no binary sniffing of files that
39
+ * aren't even tracked, no accidental inclusion of `node_modules/`.
40
+ *
41
+ * Behavior:
42
+ * - Shells out to `git ls-files` in `rootDir` to get the tracked set.
43
+ * - Skips the hardcoded allow-list paths.
44
+ * - Scans only text files (binary files and a small set of known-binary
45
+ * extensions are skipped).
46
+ * - Applies the regex `\b(aiox|synkra|synkraai|bmad)\b` (case-insensitive)
47
+ * line-by-line so the reporter can point at the exact file:line:match.
48
+ * - Returns a structured result for unit tests; the CLI wraps it.
49
+ *
50
+ * Output on violation (one block per match, matches AC 4 exactly):
51
+ *
52
+ * ❌ External reference detected
53
+ * File: <path>
54
+ * Line: <num>
55
+ * Match: <text>
56
+ *
57
+ * Fix: Remove the reference. This repo uses authorial SINAPSE voice only.
58
+ * Only LICENSE may contain these references (legal requirement).
59
+ *
60
+ * Usage:
61
+ * node scripts/validate-no-external-refs.js [--root <dir>]
62
+ *
63
+ * Exit codes:
64
+ * 0 = clean
65
+ * 1 = at least one violation
66
+ * 2 = usage / filesystem / git error
67
+ */
68
+
69
+ 'use strict';
70
+
71
+ const fs = require('fs');
72
+ const path = require('path');
73
+ const { execSync } = require('child_process');
74
+
75
+ // ── Configuration ───────────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Case-insensitive regex for any forbidden reference word. Uses `\b` to avoid
79
+ * matching inside longer identifiers (e.g., `sinapse-ai` must not hit even
80
+ * though it contains `ai`).
81
+ */
82
+ const FORBIDDEN_REGEX = /\b(aiox|synkra|synkraai|bmad)\b/gi;
83
+
84
+ /**
85
+ * Hardcoded allow-list. Every entry is a POSIX-style path relative to the
86
+ * repo root. These paths are skipped by the scanner.
87
+ *
88
+ * - LICENSE: MIT attribution may legally require naming upstream authors.
89
+ * - docs/research-synthesis-for-upgrade.md: historical process document.
90
+ * - squads/claude-code-mastery/agents/skill-craftsman.md: PERMANENT.
91
+ * The agent's documented purpose is to study and adapt external
92
+ * agile-AI methodologies, so the references serve the framework
93
+ * rather than inheriting from a fork. Story 10.23 audited the file
94
+ * and decided to keep it allow-listed indefinitely.
95
+ *
96
+ * Each entry is an EXACT path match. Directory prefixes are NOT used — this
97
+ * forces future files under `squads/claude-code-mastery/` to be BLOCKED
98
+ * until they are explicitly added, which is what we want: a new file with a
99
+ * forbidden literal should fail the validator loudly.
100
+ */
101
+ const HARDCODED_ALLOW_LIST = [
102
+ 'LICENSE', // MIT legal requirement
103
+ 'docs/research-synthesis-for-upgrade.md', // Historical process doc
104
+
105
+ // ── The validator itself + its tests ─────────────────────────────────
106
+ // These files MUST literally contain the forbidden strings: the script
107
+ // defines the regex that matches them, and the test suite builds fixtures
108
+ // that assert the regex catches each term. Without this exemption the
109
+ // validator would fail on its own source code.
110
+ 'scripts/validate-no-external-refs.js',
111
+ 'tests/scripts/validate-no-external-refs.test.js',
112
+
113
+ // ── PERMANENT: skill-craftsman methodology study ─────────────────────
114
+ // Story 10.23 audited the 6 pre-existing fork attribution files from
115
+ // Story 10.17. Five were rewritten in authorial voice and removed from
116
+ // the allow-list. This one stays: the agent's identity is built around
117
+ // studying external agile-AI methodologies and the references are
118
+ // load-bearing, not inherited noise.
119
+ 'squads/claude-code-mastery/agents/skill-craftsman.md',
120
+ ];
121
+
122
+ /**
123
+ * File extensions we treat as binary and never scan. A line-by-line regex
124
+ * scan on binary content is slow, noisy, and can produce false positives
125
+ * (e.g. a byte sequence that happens to spell a forbidden word).
126
+ */
127
+ const BINARY_EXTENSIONS = new Set([
128
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp',
129
+ '.pdf', '.zip', '.tar', '.gz', '.tgz', '.7z', '.rar',
130
+ '.mp3', '.mp4', '.mov', '.avi', '.webm', '.ogg',
131
+ '.woff', '.woff2', '.ttf', '.otf', '.eot',
132
+ '.exe', '.dll', '.so', '.dylib',
133
+ '.lock', '.lockb',
134
+ ]);
135
+
136
+ /**
137
+ * Maximum file size we scan (8 MB). Anything larger is almost certainly a
138
+ * binary blob or a generated artifact and is not worth the IO cost.
139
+ */
140
+ const MAX_SCAN_BYTES = 8 * 1024 * 1024;
141
+
142
+ // ── Allow-list matching ─────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Exact-match allow-list check. Directory prefixes are NOT supported by
146
+ * design: see the comment on HARDCODED_ALLOW_LIST.
147
+ */
148
+ function isAllowListed(relPath) {
149
+ const normalized = relPath.replace(/\\/g, '/');
150
+ return HARDCODED_ALLOW_LIST.indexOf(normalized) !== -1;
151
+ }
152
+
153
+ // ── File discovery ──────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Return the list of files tracked by git in `rootDir`, as POSIX-style
157
+ * relative paths. This is the exact set of files that will be published.
158
+ *
159
+ * We use `git ls-files -z` (null-delimited) so filenames with spaces or
160
+ * other special chars are handled correctly on Windows and Unix.
161
+ *
162
+ * @param {string} rootDir absolute path to a git repository root
163
+ * @returns {string[]}
164
+ * @throws if `git ls-files` cannot be run (no git, not a repo, etc.)
165
+ */
166
+ function listTrackedFiles(rootDir) {
167
+ let raw;
168
+ try {
169
+ raw = execSync('git ls-files -z', {
170
+ cwd: rootDir,
171
+ encoding: 'utf8',
172
+ maxBuffer: 64 * 1024 * 1024, // 64 MB — plenty for any repo we care about
173
+ stdio: ['ignore', 'pipe', 'pipe'],
174
+ });
175
+ } catch (err) {
176
+ const msg = err && err.message ? err.message : String(err);
177
+ throw new Error(
178
+ 'git ls-files failed in ' + rootDir + ': ' + msg +
179
+ '\n(The validator requires a git repository as its root.)',
180
+ );
181
+ }
182
+ // Split on NUL; drop the trailing empty entry that `-z` always leaves.
183
+ return raw
184
+ .split('\0')
185
+ .filter((entry) => entry.length > 0)
186
+ .map((entry) => entry.replace(/\\/g, '/'));
187
+ }
188
+
189
+ // ── Scanner ─────────────────────────────────────────────────────────────────
190
+
191
+ /**
192
+ * Detect whether a file is likely binary by checking its extension and a
193
+ * quick sniff of the first 4 KB for null bytes.
194
+ */
195
+ function isBinaryFile(absPath) {
196
+ const ext = path.extname(absPath).toLowerCase();
197
+ if (BINARY_EXTENSIONS.has(ext)) return true;
198
+ try {
199
+ const fd = fs.openSync(absPath, 'r');
200
+ const buf = Buffer.alloc(4096);
201
+ const n = fs.readSync(fd, buf, 0, 4096, 0);
202
+ fs.closeSync(fd);
203
+ for (let i = 0; i < n; i++) {
204
+ if (buf[i] === 0) return true;
205
+ }
206
+ } catch {
207
+ return true; // unreadable → treat as binary
208
+ }
209
+ return false;
210
+ }
211
+
212
+ /**
213
+ * Scan a single file. Returns an array of violation objects; empty if clean.
214
+ */
215
+ function scanFile(rootDir, relPath) {
216
+ if (isAllowListed(relPath)) return [];
217
+ const absPath = path.join(rootDir, relPath);
218
+ let stat;
219
+ try {
220
+ stat = fs.statSync(absPath);
221
+ } catch {
222
+ return [];
223
+ }
224
+ if (!stat.isFile()) return [];
225
+ if (stat.size > MAX_SCAN_BYTES) return [];
226
+ if (isBinaryFile(absPath)) return [];
227
+
228
+ let content;
229
+ try {
230
+ content = fs.readFileSync(absPath, 'utf8');
231
+ } catch {
232
+ return [];
233
+ }
234
+
235
+ const lines = content.split(/\r?\n/);
236
+ const violations = [];
237
+ for (let i = 0; i < lines.length; i++) {
238
+ const line = lines[i];
239
+ // Reset regex state for each line; /g keeps lastIndex sticky.
240
+ FORBIDDEN_REGEX.lastIndex = 0;
241
+ let match;
242
+ while ((match = FORBIDDEN_REGEX.exec(line)) !== null) {
243
+ violations.push({
244
+ file: relPath,
245
+ line: i + 1,
246
+ match: match[0],
247
+ });
248
+ }
249
+ }
250
+ return violations;
251
+ }
252
+
253
+ /**
254
+ * Public API: scan the repository rooted at `rootDir` and return
255
+ * { ok, scanned, violations }
256
+ *
257
+ * The function uses `git ls-files` to determine the scan set, so `rootDir`
258
+ * MUST be a git repository root (or a directory inside one). Tests should
259
+ * create a fixture with `git init` + `git add` to exercise this.
260
+ *
261
+ * @param {string} rootDir
262
+ * @returns {{ok: boolean, scanned: string[], violations: Array<{file: string, line: number, match: string}>}}
263
+ */
264
+ function validateNoExternalRefs(rootDir) {
265
+ const files = listTrackedFiles(rootDir);
266
+ const violations = [];
267
+ for (const relPath of files) {
268
+ violations.push(...scanFile(rootDir, relPath));
269
+ }
270
+ return { ok: violations.length === 0, scanned: files, violations };
271
+ }
272
+
273
+ // ── Reporter ────────────────────────────────────────────────────────────────
274
+
275
+ /**
276
+ * Format a violation block exactly as AC 4 specifies.
277
+ */
278
+ function formatViolation(v) {
279
+ return [
280
+ '❌ External reference detected',
281
+ 'File: ' + v.file,
282
+ 'Line: ' + v.line,
283
+ 'Match: ' + v.match,
284
+ '',
285
+ 'Fix: Remove the reference. This repo uses authorial SINAPSE voice only.',
286
+ 'Only LICENSE may contain these references (legal requirement).',
287
+ '',
288
+ ].join('\n');
289
+ }
290
+
291
+ function formatReport(result) {
292
+ if (result.ok) {
293
+ return (
294
+ '\n=== validate-no-external-refs ===\n' +
295
+ 'Scanned ' + result.scanned.length + ' file(s).\n' +
296
+ 'OK — no external references detected.\n\n'
297
+ );
298
+ }
299
+ const blocks = result.violations.map(formatViolation);
300
+ return (
301
+ '\n=== validate-no-external-refs ===\n' +
302
+ 'Scanned ' + result.scanned.length + ' file(s).\n' +
303
+ 'FAIL — ' + result.violations.length + ' external reference(s):\n\n' +
304
+ blocks.join('\n')
305
+ );
306
+ }
307
+
308
+ // ── CLI entry point ─────────────────────────────────────────────────────────
309
+
310
+ function parseArgs(argv) {
311
+ const args = { root: process.cwd() };
312
+ for (let i = 0; i < argv.length; i++) {
313
+ const a = argv[i];
314
+ if (a === '--root' || a === '-r') {
315
+ args.root = argv[++i];
316
+ } else if (a === '--help' || a === '-h') {
317
+ args.help = true;
318
+ }
319
+ }
320
+ return args;
321
+ }
322
+
323
+ function printUsage() {
324
+ process.stdout.write(
325
+ [
326
+ 'Usage: node scripts/validate-no-external-refs.js [--root <dir>]',
327
+ '',
328
+ 'Scans the repo for any competitive framework reference and fails if',
329
+ 'any match is found outside the hardcoded allow-list (LICENSE, historical',
330
+ 'process doc, pre-existing upstream fork attribution). Uses `git ls-files`',
331
+ 'as the source of truth so only publishable tracked files are scanned.',
332
+ '',
333
+ 'Options:',
334
+ ' --root, -r <dir> Root directory to scan (defaults to cwd).',
335
+ ' --help, -h Show this help.',
336
+ '',
337
+ ].join('\n'),
338
+ );
339
+ }
340
+
341
+ function main() {
342
+ const args = parseArgs(process.argv.slice(2));
343
+ if (args.help) {
344
+ printUsage();
345
+ process.exit(0);
346
+ }
347
+ try {
348
+ const rootAbs = path.resolve(args.root);
349
+ if (!fs.existsSync(rootAbs)) {
350
+ process.stderr.write('error: root directory not found: ' + rootAbs + '\n');
351
+ process.exit(2);
352
+ }
353
+ const result = validateNoExternalRefs(rootAbs);
354
+ process.stdout.write(formatReport(result));
355
+ process.exit(result.ok ? 0 : 1);
356
+ } catch (err) {
357
+ process.stderr.write('error: ' + (err && err.message ? err.message : err) + '\n');
358
+ process.exit(2);
359
+ }
360
+ }
361
+
362
+ // Exports for unit testing.
363
+ module.exports = {
364
+ FORBIDDEN_REGEX,
365
+ HARDCODED_ALLOW_LIST,
366
+ isAllowListed,
367
+ listTrackedFiles,
368
+ scanFile,
369
+ validateNoExternalRefs,
370
+ formatViolation,
371
+ formatReport,
372
+ };
373
+
374
+ if (require.main === module) {
375
+ main();
376
+ }
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-squad-orqx — Story 10.28
4
+ *
5
+ * Verifies that every squad under `squads/` exposes a reachable
6
+ * orchestrator agent (`*-orqx.md`) with the minimum required YAML
7
+ * metadata block. Companion to the existing scripts/validate-agents.js
8
+ * which validates the 12 core framework agents under
9
+ * `.sinapse-ai/development/agents/`.
10
+ *
11
+ * Checks per orqx file:
12
+ * 1. File exists at squads/{squad}/agents/{name}-orqx.md
13
+ * 2. Contains a YAML code block with `agent:` mapping
14
+ * 3. agent.name is a non-empty string
15
+ * 4. agent.id is a non-empty string and matches `{squad}/{name}-orqx`
16
+ * 5. agent.title is a non-empty string
17
+ * 6. persona.role exists
18
+ *
19
+ * Squad-level checks:
20
+ * - Every directory under squads/ that has agents/ MUST have at least
21
+ * one *-orqx.md file inside.
22
+ *
23
+ * Exit codes:
24
+ * 0 all squads have reachable orqx agents with valid metadata
25
+ * 1 any squad missing an orqx OR any orqx fails metadata checks
26
+ *
27
+ * Usage:
28
+ * node scripts/validate-squad-orqx.js
29
+ * node scripts/validate-squad-orqx.js --json
30
+ * node scripts/validate-squad-orqx.js --quiet
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+
38
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
39
+ const SQUADS_DIR = path.join(PROJECT_ROOT, 'squads');
40
+
41
+ function parseArgs(argv = process.argv.slice(2)) {
42
+ const args = new Set(argv);
43
+ return {
44
+ quiet: args.has('--quiet') || args.has('-q'),
45
+ json: args.has('--json'),
46
+ };
47
+ }
48
+
49
+ function listSquads(squadsDir = SQUADS_DIR) {
50
+ if (!fs.existsSync(squadsDir)) return [];
51
+ return fs
52
+ .readdirSync(squadsDir, { withFileTypes: true })
53
+ .filter((d) => d.isDirectory())
54
+ .map((d) => d.name)
55
+ .sort();
56
+ }
57
+
58
+ function findOrqxFiles(squadName, squadsDir = SQUADS_DIR) {
59
+ const agentsDir = path.join(squadsDir, squadName, 'agents');
60
+ if (!fs.existsSync(agentsDir)) return [];
61
+ return fs
62
+ .readdirSync(agentsDir)
63
+ .filter((f) => f.endsWith('-orqx.md'))
64
+ .map((f) => path.join(agentsDir, f));
65
+ }
66
+
67
+ // parseOrqxFormat detects either of the two valid orqx file formats:
68
+ // 1. YAML block (`\`\`\`yaml ... \`\`\``) with agent: + persona: keys
69
+ // 2. Markdown headings (`# Agent: ...`, `## Identidade`, `## Role`)
70
+ // Returns { format, agent, persona } or null if neither shape recognized.
71
+ function parseOrqxFormat(content) {
72
+ // Format 0: YAML frontmatter (--- ... ---)
73
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
74
+ if (frontmatterMatch) {
75
+ const block = frontmatterMatch[1];
76
+ const data = { format: 'frontmatter', agent: {}, persona: {} };
77
+ const nameMatch = block.match(/^name:\s*"?([^"\n]+?)"?\s*$/m);
78
+ if (nameMatch) {
79
+ data.agent.id = nameMatch[1].trim();
80
+ data.agent.name = nameMatch[1].trim();
81
+ }
82
+ const descMatch = block.match(/^description:\s*\|?\s*\n((?:\s{2}.+\n?)+)/);
83
+ if (descMatch) {
84
+ data.agent.title = descMatch[1].trim().split('\n')[0].trim();
85
+ data.persona.role = descMatch[1].trim().split('\n')[0].trim();
86
+ } else {
87
+ const inlineDescMatch = block.match(/^description:\s*(.+)$/m);
88
+ if (inlineDescMatch) {
89
+ data.agent.title = inlineDescMatch[1].trim();
90
+ data.persona.role = inlineDescMatch[1].trim();
91
+ }
92
+ }
93
+ if (Object.keys(data.agent).length > 0) return data;
94
+ }
95
+
96
+ const yamlMatch = content.match(/```yaml\n([\s\S]*?)\n```/);
97
+ if (yamlMatch) {
98
+ const block = yamlMatch[1];
99
+ const data = { format: 'yaml', agent: {}, persona: {} };
100
+ const agentMatch = block.match(/agent:\s*\n((?:\s{2}.+\n?)+)/);
101
+ if (agentMatch) {
102
+ const lines = agentMatch[1].split('\n');
103
+ for (const line of lines) {
104
+ const m = line.match(/^\s{2}(\w+):\s*"?([^"\n]+?)"?\s*$/);
105
+ if (m) data.agent[m[1]] = m[2];
106
+ }
107
+ }
108
+ const roleMatch = block.match(/persona:[\s\S]*?role:\s*"?([^"\n]+?)"?$/m);
109
+ if (roleMatch) data.persona.role = roleMatch[1].trim();
110
+ // Fall through to markdown branch if YAML extraction yielded nothing.
111
+ if (Object.keys(data.agent).length > 0) return data;
112
+ }
113
+
114
+ // Markdown-headings format (squad-design and friends)
115
+ if (/^#\s+Agent:\s/m.test(content) || /\*\*ID:\*\*/.test(content)) {
116
+ const data = { format: 'markdown', agent: {}, persona: {} };
117
+ const titleMatch = content.match(/^#\s+(?:Agent:\s+)?(.+?)$/m);
118
+ if (titleMatch) data.agent.title = titleMatch[1].trim();
119
+
120
+ const idMatch = content.match(/\*\*ID:\*\*\s*`?([^\s`]+)`?/);
121
+ if (idMatch) data.agent.id = idMatch[1].trim();
122
+
123
+ const nameMatch = content.match(/\*\*Nome:\*\*\s*([^\n]+)/);
124
+ if (nameMatch) data.agent.name = nameMatch[1].trim();
125
+
126
+ const roleHeadingMatch = content.match(/^##\s+Role\s*\n+([\s\S]+?)(?:\n##|\n$)/m);
127
+ if (roleHeadingMatch) data.persona.role = roleHeadingMatch[1].trim().split('\n')[0];
128
+
129
+ return data;
130
+ }
131
+
132
+ // Table-based identity format (squad-claude orqx files)
133
+ if (/\*\*Agent ID\*\*/.test(content) || /\*\*Name\*\*\s*\|/.test(content)) {
134
+ const data = { format: 'markdown-table', agent: {}, persona: {} };
135
+ const titleMatch = content.match(/^#\s+(.+?)$/m);
136
+ if (titleMatch) data.agent.title = titleMatch[1].trim();
137
+
138
+ const nameMatch = content.match(/\*\*Name\*\*\s*\|\s*([^|\n]+)/);
139
+ if (nameMatch) data.agent.name = nameMatch[1].trim();
140
+
141
+ const idMatch = content.match(/\*\*Agent ID\*\*\s*\|\s*`?@?([^|`\n]+?)`?\s*\|/);
142
+ if (idMatch) data.agent.id = idMatch[1].trim();
143
+
144
+ const roleHeadingMatch = content.match(/^##\s+Role\s*\n+([\s\S]+?)(?:\n##|\n$)/m);
145
+ if (roleHeadingMatch) data.persona.role = roleHeadingMatch[1].trim().split('\n')[0];
146
+
147
+ return data;
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ function validateOrqxFile(filePath) {
154
+ const findings = [];
155
+ const fileName = path.basename(filePath);
156
+ const relPath = path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
157
+ const squadName = relPath.split('/')[1];
158
+ const expectedId = `${squadName}/${fileName.replace('.md', '')}`;
159
+
160
+ let content;
161
+ try {
162
+ content = fs.readFileSync(filePath, 'utf8');
163
+ } catch (err) {
164
+ findings.push({ level: 'error', rule: 'read', message: `cannot read: ${err.message}` });
165
+ return findings;
166
+ }
167
+
168
+ const data = parseOrqxFormat(content);
169
+ if (!data) {
170
+ findings.push({
171
+ level: 'error',
172
+ rule: 'unrecognized-format',
173
+ message: 'no YAML block and no Markdown identity heading found',
174
+ });
175
+ return findings;
176
+ }
177
+
178
+ // At least one of name/id/title must be present so the agent is reachable.
179
+ const hasIdentity = Boolean(data.agent.name || data.agent.id || data.agent.title);
180
+ if (!hasIdentity) {
181
+ findings.push({
182
+ level: 'error',
183
+ rule: 'agent-identity',
184
+ message: 'no agent.name / agent.id / agent.title found in either format',
185
+ });
186
+ return findings;
187
+ }
188
+
189
+ // ID match is a soft check — Markdown format often uses bare ID like
190
+ // "design-orqx" without the squad prefix. Warn but don't error.
191
+ if (data.agent.id && data.agent.id !== expectedId) {
192
+ const fileBaseId = path.basename(filePath, '.md');
193
+ if (data.agent.id !== fileBaseId) {
194
+ findings.push({
195
+ level: 'warn',
196
+ rule: 'id-mismatch',
197
+ message: `agent.id="${data.agent.id}" does not match "${expectedId}" or "${fileBaseId}"`,
198
+ });
199
+ }
200
+ }
201
+
202
+ if (!data.persona || !data.persona.role) {
203
+ findings.push({
204
+ level: 'warn',
205
+ rule: 'persona-role',
206
+ message: 'persona.role / Role section is missing',
207
+ });
208
+ }
209
+
210
+ return findings;
211
+ }
212
+
213
+ function validateAllSquads(squadsDir = SQUADS_DIR) {
214
+ const squads = listSquads(squadsDir);
215
+ const results = [];
216
+ for (const squad of squads) {
217
+ const orqxFiles = findOrqxFiles(squad, squadsDir);
218
+ if (orqxFiles.length === 0) {
219
+ // Some squad directories don't have agents/ at all (legacy/utility),
220
+ // skip those silently. Only flag as ERROR if agents/ exists but no orqx.
221
+ const agentsDir = path.join(squadsDir, squad, 'agents');
222
+ if (fs.existsSync(agentsDir)) {
223
+ results.push({
224
+ squad,
225
+ orqxFiles: [],
226
+ findings: [
227
+ { level: 'error', rule: 'no-orqx', message: 'squad has agents/ but no *-orqx.md' },
228
+ ],
229
+ });
230
+ }
231
+ continue;
232
+ }
233
+ for (const filePath of orqxFiles) {
234
+ results.push({ squad, orqxFile: filePath, findings: validateOrqxFile(filePath) });
235
+ }
236
+ }
237
+ return results;
238
+ }
239
+
240
+ function summarize(results) {
241
+ const summary = { total: results.length, pass: 0, warn: 0, error: 0 };
242
+ for (const r of results) {
243
+ const hasError = r.findings.some((f) => f.level === 'error');
244
+ const hasWarn = r.findings.some((f) => f.level === 'warn');
245
+ if (hasError) summary.error += 1;
246
+ else if (hasWarn) summary.warn += 1;
247
+ else summary.pass += 1;
248
+ }
249
+ return summary;
250
+ }
251
+
252
+ function formatReport(results, summary) {
253
+ const lines = [];
254
+ for (const r of results) {
255
+ const tag = r.findings.some((f) => f.level === 'error')
256
+ ? 'ERROR'
257
+ : r.findings.some((f) => f.level === 'warn')
258
+ ? 'WARN '
259
+ : 'PASS ';
260
+ const label = r.orqxFile ? path.relative(PROJECT_ROOT, r.orqxFile).replace(/\\/g, '/') : `${r.squad} (no orqx)`;
261
+ lines.push(`${tag} ${label}`);
262
+ for (const f of r.findings) {
263
+ lines.push(` [${f.level}] ${f.rule}: ${f.message}`);
264
+ }
265
+ }
266
+ lines.push('');
267
+ lines.push(
268
+ `Summary: ${summary.total} orqx file(s) — ${summary.pass} pass, ${summary.warn} warn, ${summary.error} error`,
269
+ );
270
+ return lines.join('\n');
271
+ }
272
+
273
+ function main() {
274
+ const args = parseArgs();
275
+ const results = validateAllSquads();
276
+ const summary = summarize(results);
277
+
278
+ if (args.json) {
279
+ console.log(JSON.stringify({ results, summary }, null, 2));
280
+ } else if (!args.quiet) {
281
+ console.log('=== validate-squad-orqx ===');
282
+ console.log(formatReport(results, summary));
283
+ }
284
+
285
+ return summary.error > 0 ? 1 : 0;
286
+ }
287
+
288
+ if (require.main === module) {
289
+ process.exitCode = main();
290
+ }
291
+
292
+ module.exports = {
293
+ parseArgs,
294
+ listSquads,
295
+ findOrqxFiles,
296
+ parseOrqxFormat,
297
+ validateOrqxFile,
298
+ validateAllSquads,
299
+ summarize,
300
+ formatReport,
301
+ main,
302
+ };