sinapse-ai 1.9.1 → 1.10.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.
@@ -7,7 +7,7 @@
7
7
  # - SHA256 hashes for change detection
8
8
  # - File types for categorization
9
9
  #
10
- version: 1.9.1
10
+ version: 1.10.0
11
11
  generator: scripts/generate-install-manifest.js
12
12
  file_count: 1180
13
13
  files:
package/AGENTS.md CHANGED
@@ -23,14 +23,14 @@ These rules are HARD CONSTRAINTS. They apply to every action you take, in any ID
23
23
 
24
24
  ## Project Context
25
25
 
26
- SINAPSE is a meta-framework that orchestrates AI agents into specialized squads for complex development workflows. It runs inside Claude Code and enforces a formal Constitution with 10 articles governing CLI-first architecture, agent authority, documentation-first development, security, and safe collaboration.
26
+ SINAPSE is a meta-framework that orchestrates AI agents into specialized squads for complex development workflows. It runs inside Claude Code and enforces a formal Constitution with 11 articles governing CLI-first architecture, agent authority, documentation-first development, security, and safe collaboration.
27
27
 
28
28
  ### Architecture
29
29
 
30
30
  - **CLI First** -- All intelligence lives in the CLI. Dashboards observe, never control.
31
31
  - **4-Layer Boundary** -- L1 (core, immutable) / L2 (templates, extend-only) / L3 (config, mutable) / L4 (runtime, always modify).
32
32
  - **Documentation-First** -- No code without a validated story. Pipeline: Epic -> Story -> Validation -> Implementation.
33
- - **Constitution** -- 10 articles with automatic gates that block violations. See `.sinapse-ai/constitution.md`.
33
+ - **Constitution** -- 11 articles with automatic gates that block violations. See `.sinapse-ai/constitution.md`.
34
34
 
35
35
  ### Project Structure
36
36
 
package/CHANGELOG.md CHANGED
@@ -1,9 +1,19 @@
1
- ## [1.9.1](https://github.com/caioimori/sinapse-ai/compare/1.9.0...1.9.1) (2026-06-18)
1
+ ## [1.10.0](https://github.com/caioimori/sinapse-ai/compare/1.9.1...1.10.0) (2026-06-18)
2
+
3
+ ### Features
4
+
5
+ * **article-vii:** guarda contagem de artigos da Constitution contra drift ([c010fda](https://github.com/caioimori/sinapse-ai/commit/c010fda484a8d2eb0292ee0c583a60ae1d13a745))
6
+ * **cli:** torna todos os comandos descobriveis no --help + guard de regressao ([1eba7d5](https://github.com/caioimori/sinapse-ai/commit/1eba7d5bb746dd241282e5d33aaa890f52d6d156))
2
7
 
3
8
  ### Bug Fixes
4
9
 
5
- * **audit:** correções da auditoria adversarial pós-1.9.0 ([3cef25e](https://github.com/caioimori/sinapse-ai/commit/3cef25e4ad6f9fa504f17b556ce16071bb3986ac))
6
- * **release:** remove @semantic-release/git (branch protegida rejeita push direto na main) ([0c62b90](https://github.com/caioimori/sinapse-ai/commit/0c62b90e8fa64160f2f0cef5b29daef871abe0f7))
10
+ * **install:** reinstall preserva arquivos L3 user-owned (core-config, MEMORY.md) ([b2ab1a5](https://github.com/caioimori/sinapse-ai/commit/b2ab1a56025f12fc9a2123355541eaa41cbbf74b))
11
+ * **security:** corrige 2 vulns moderadas que chegam a quem instala (js-yaml, tar) ([947694b](https://github.com/caioimori/sinapse-ai/commit/947694b8eee3f6ad6da6bb342a73c78fe8ab2658))
12
+ * **security:** elimina interpolacao de shell no setx PATH do install (Windows) ([a4e9cae](https://github.com/caioimori/sinapse-ai/commit/a4e9caeda475b5873169151f811aa1ed92ff8181))
13
+
14
+ ### Documentation
15
+
16
+ * corrige contagem da Constitution no README (10->11 artigos, 6->7 NON-NEGOTIABLE) ([40713a5](https://github.com/caioimori/sinapse-ai/commit/40713a51dceda3477fd4b33750e4cfe188cff086))
7
17
 
8
18
  # Changelog
9
19
 
package/README.en.md CHANGED
@@ -33,7 +33,7 @@ Generative AI has a known problem: the more you ask of it, the worse it gets. A
33
33
 
34
34
  SINAPSE solves this the way human teams solve it: **coordinated specialization**. Instead of one tired generalist, you have 172 agents in 17 squads, each with a defined role, its own knowledge base, and executable tasks. An orchestrator routes your request to whoever actually knows how to solve it -- automatically, without you needing to memorize agent names or commands.
35
35
 
36
- The differential isn't just the quantity of agents. It's **real governance**: 19 active hooks intercept operations at runtime, a Constitution with 10 articles governs the framework, and 6 of those articles are NON-NEGOTIABLE -- violations are blocked before execution, not detected afterwards. **Speed with rigor, without choosing between the two.**
36
+ The differential isn't just the quantity of agents. It's **real governance**: 19 active hooks intercept operations at runtime, a Constitution with 11 articles governs the framework, and 7 of those articles are NON-NEGOTIABLE -- violations are blocked before execution, not detected afterwards. **Speed with rigor, without choosing between the two.**
37
37
 
38
38
  ---
39
39
 
@@ -174,7 +174,7 @@ Deny rules in `.claude/settings.json` enforce this deterministically. **Framewor
174
174
 
175
175
  ### Constitution
176
176
 
177
- SINAPSE is governed by a formal Constitution with 10 articles and 19 enforcement hooks:
177
+ SINAPSE is governed by a formal Constitution with 11 articles and 19 enforcement hooks:
178
178
 
179
179
  | Article | Principle | Severity |
180
180
  |---------|-----------|----------|
@@ -188,8 +188,9 @@ SINAPSE is governed by a formal Constitution with 10 articles and 19 enforcement
188
188
  | VIII | Mandatory Delegation | NON-NEGOTIABLE |
189
189
  | IX | Safe Collaboration | NON-NEGOTIABLE |
190
190
  | X | Security & Data Protection | NON-NEGOTIABLE |
191
+ | XI | Conservative Default | MUST |
191
192
 
192
- 6 articles are NON-NEGOTIABLE -- violations are automatically blocked before execution.
193
+ 7 articles are NON-NEGOTIABLE -- violations are automatically blocked before execution.
193
194
 
194
195
  ---
195
196
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
4
4
  [![CI](https://github.com/caioimori/sinapse-ai/actions/workflows/ci.yml/badge.svg)](https://github.com/caioimori/sinapse-ai/actions/workflows/ci.yml)
5
5
  [![Tests](https://img.shields.io/badge/tests-11014%20passed-success)](https://github.com/caioimori/sinapse-ai/actions/workflows/ci.yml)
6
- [![Constitution](https://img.shields.io/badge/Constitution-10%20articles-blueviolet)](.sinapse-ai/constitution.md)
6
+ [![Constitution](https://img.shields.io/badge/Constitution-11%20articles-blueviolet)](.sinapse-ai/constitution.md)
7
7
 
8
8
  ```
9
9
  ____ ___ _ _ _ ____ ____ _____
@@ -35,7 +35,7 @@ IA generativa tem um problema conhecido: quanto mais voce pede, pior fica. Um un
35
35
 
36
36
  SINAPSE resolve isso do jeito que times humanos resolvem: **especializacao coordenada**. Em vez de um generalista cansado, voce tem 172 agentes em 17 squads, cada um com papel definido, knowledge base propria e tasks executaveis. Um orquestrador roteia seu pedido para quem realmente sabe resolver — automaticamente, sem voce precisar decorar agent names ou comandos.
37
37
 
38
- O diferencial nao e apenas quantidade de agentes. E **governanca real**: 13 hooks ativos interceptam operacoes em tempo de execucao, uma Constitution com 10 artigos rege o framework, e 6 desses artigos sao NON-NEGOTIABLE — violacoes sao bloqueadas antes de executar, nao detectadas depois. **Velocidade com rigor, sem escolher entre os dois.**
38
+ O diferencial nao e apenas quantidade de agentes. E **governanca real**: 13 hooks ativos interceptam operacoes em tempo de execucao, uma Constitution com 11 artigos rege o framework, e 7 desses artigos sao NON-NEGOTIABLE — violacoes sao bloqueadas antes de executar, nao detectadas depois. **Velocidade com rigor, sem escolher entre os dois.**
39
39
 
40
40
  ---
41
41
 
@@ -137,8 +137,6 @@ Cada release passa por uma matriz de instalacao de 27 combinacoes (3 OSes x 3 pa
137
137
 
138
138
  **Sobre Yarn v1 no Windows:** Yarn v1 (Classic) esta em modo manutencao desde 2020, com Yarn v2+ (Berry) como sucessor oficial. A combinacao Windows + Yarn v1 apresenta falhas de resolucao de wrapper que nao compensam investigar por ser ecossistema em declinio. Usuarios Windows em Yarn v1 devem migrar para Yarn v2+ (recomendado) ou usar npm/pnpm. macOS e Linux em Yarn v1 continuam suportados.
139
139
 
140
- Registro completo da decisao e resultados: [docs/audits/install-matrix-2026-04-16.md](docs/audits/install-matrix-2026-04-16.md).
141
-
142
140
  ### "Como atualizar sem perder minhas customizacoes?"
143
141
 
144
142
  ```bash
@@ -215,7 +213,7 @@ Deny rules em `.claude/settings.json` reforcam isso deterministicamente. **Updat
215
213
 
216
214
  ### Constitution
217
215
 
218
- O SINAPSE e governado por uma Constitution formal com 10 artigos e 13 hooks de enforcement:
216
+ O SINAPSE e governado por uma Constitution formal com 11 artigos e 13 hooks de enforcement:
219
217
 
220
218
  | Artigo | Principio | Severidade |
221
219
  |--------|-----------|------------|
@@ -229,8 +227,9 @@ O SINAPSE e governado por uma Constitution formal com 10 artigos e 13 hooks de e
229
227
  | VIII | Mandatory Delegation | NON-NEGOTIABLE |
230
228
  | IX | Safe Collaboration | NON-NEGOTIABLE |
231
229
  | X | Security & Data Protection | NON-NEGOTIABLE |
230
+ | XI | Conservative Default | MUST |
232
231
 
233
- 6 artigos sao NON-NEGOTIABLE — violacoes sao bloqueadas automaticamente antes de executar.
232
+ 7 artigos sao NON-NEGOTIABLE — violacoes sao bloqueadas automaticamente antes de executar.
234
233
 
235
234
  ---
236
235
 
@@ -1,7 +1,7 @@
1
1
  // bin/commands/install.js — `sinapse-ai install` (global) command + helpers.
2
2
  // Story GA-1.2 — extracted from bin/cli.js.
3
3
 
4
- const { execSync } = require('child_process');
4
+ const { execSync, execFileSync } = require('child_process');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { getLogger } = require('../../.sinapse-ai/core/logger');
@@ -893,11 +893,14 @@ function ensurePathWindows() {
893
893
  return;
894
894
  }
895
895
 
896
- execSync(`setx PATH "${newPath}"`, { encoding: 'utf8', stdio: 'pipe' });
896
+ // execFileSync (no shell) never interpolate the current PATH into a
897
+ // shell command string. A PATH value containing `"`/`&`/`^` would break
898
+ // the command or allow injection; passing args as an array avoids it.
899
+ execFileSync('setx', ['PATH', newPath], { encoding: 'utf8', stdio: 'pipe' });
897
900
  logger.always(` ${GREEN}OK${NC} Added ~/bin to Windows User PATH`);
898
901
  } catch {
899
902
  try {
900
- execSync(`setx PATH "%USERPROFILE%\\bin"`, { encoding: 'utf8', stdio: 'pipe' });
903
+ execFileSync('setx', ['PATH', '%USERPROFILE%\\bin'], { encoding: 'utf8', stdio: 'pipe' });
901
904
  logger.always(` ${GREEN}OK${NC} Created Windows User PATH with ~/bin`);
902
905
  } catch {
903
906
  logger.always(` ${YELLOW}WARN${NC} Could not modify PATH. Add manually: ${BIN_DIR}`);
package/bin/sinapse.js CHANGED
@@ -73,6 +73,7 @@ USAGE:
73
73
  sinapse install # Install SINAPSE in current project
74
74
  sinapse init <name> # Create new project
75
75
  sinapse update # Update to latest version
76
+ sinapse uninstall # Remove SINAPSE from the project (--keep-data)
76
77
  sinapse brand # Re-apply SINAPSE branding (after Claude update)
77
78
  sinapse validate # Validate installation integrity
78
79
  sinapse info # Show system info
@@ -119,6 +120,25 @@ SERVICE DISCOVERY:
119
120
  sinapse workers search "transform" --tags=etl,data
120
121
  sinapse workers search "api" --format=json
121
122
 
123
+ GENERATE & CREATE:
124
+ sinapse generate <prd|adr|story|epic|task> # Generate a document from a template
125
+ sinapse create <agent|task|workflow> # Scaffold a new framework component
126
+
127
+ ORCHESTRATION:
128
+ sinapse orchestrate <story-id> # Run the autonomous dev pipeline for a story
129
+ sinapse orchestrate --status # Show / --stop / --resume a running pipeline
130
+ sinapse mode [explore|ask|auto] # Show or set the agent permission mode (--cycle)
131
+
132
+ INTELLIGENCE & HEALTH:
133
+ sinapse health # Framework health analytics (--deep for full scan)
134
+ sinapse graph --deps # Dependency & stats dashboard (--format, --watch)
135
+ sinapse performance # Squad & agent performance ranking (--top N)
136
+ sinapse routing-intel analyze # Routing intelligence analysis
137
+
138
+ INTEGRATIONS & LICENSE:
139
+ sinapse mcp setup # Configure global MCP servers (setup/link/status/add)
140
+ sinapse pro status # SINAPSE Pro license (activate/status/deactivate/features)
141
+
122
142
  EXAMPLES:
123
143
  # Install in current directory
124
144
  npx sinapse-ai@latest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinapse-ai",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "SINAPSE AI: Framework de orquestracao de IA — 17 squads, 172 agentes especializados",
5
5
  "bin": {
6
6
  "sinapse": "bin/sinapse.js",
@@ -141,8 +141,29 @@ async function generateVersionJson(options) {
141
141
  * @param {boolean} replaceRoot - Whether to replace {root} placeholders
142
142
  * @returns {Promise<boolean>} Success status
143
143
  */
144
- async function copyFileWithRootReplacement(sourcePath, destPath, replaceRoot = true) {
144
+ /**
145
+ * Predicate: is this destination a user-owned L3 file that must survive a
146
+ * re-install? Matches the boundary contract (core-config.yaml + agent
147
+ * MEMORY.md are L3 "mutable / user-owned") and the README promise that
148
+ * re-running install is an idempotent upsert that preserves customizations.
149
+ * @param {string} destPath - Absolute destination path
150
+ * @returns {boolean}
151
+ */
152
+ function isUserOwnedL3(destPath) {
153
+ const norm = String(destPath).replace(/\\/g, '/');
154
+ if (norm.endsWith('/.sinapse-ai/core-config.yaml')) return true;
155
+ if (/\/\.sinapse-ai\/development\/agents\/[^/]+\/MEMORY\.md$/.test(norm)) return true;
156
+ return false;
157
+ }
158
+
159
+ async function copyFileWithRootReplacement(sourcePath, destPath, replaceRoot = true, preserveExisting = null) {
145
160
  try {
161
+ // Preserve user-owned L3 files (core-config.yaml, agent MEMORY.md) when they
162
+ // already exist — a re-install must not silently wipe the user's config.
163
+ if (preserveExisting && preserveExisting(destPath) && await fs.pathExists(destPath)) {
164
+ return 'preserved';
165
+ }
166
+
146
167
  await fs.ensureDir(path.dirname(destPath));
147
168
 
148
169
  // Check if file needs {root} replacement (.md, .yaml, .yml)
@@ -171,7 +192,7 @@ async function copyFileWithRootReplacement(sourcePath, destPath, replaceRoot = t
171
192
  * @param {Function} onProgress - Progress callback
172
193
  * @returns {Promise<string[]>} List of copied files (relative paths)
173
194
  */
174
- async function copyDirectoryWithRootReplacement(sourceDir, destDir, onProgress = null) {
195
+ async function copyDirectoryWithRootReplacement(sourceDir, destDir, onProgress = null, preserveExisting = null) {
175
196
  const copiedFiles = [];
176
197
 
177
198
  if (!await fs.pathExists(sourceDir)) {
@@ -193,11 +214,13 @@ async function copyDirectoryWithRootReplacement(sourceDir, destDir, onProgress =
193
214
  }
194
215
 
195
216
  if (item.isDirectory()) {
196
- const subFiles = await copyDirectoryWithRootReplacement(sourcePath, destPath, onProgress);
217
+ const subFiles = await copyDirectoryWithRootReplacement(sourcePath, destPath, onProgress, preserveExisting);
197
218
  copiedFiles.push(...subFiles);
198
219
  } else {
199
- const success = await copyFileWithRootReplacement(sourcePath, destPath);
200
- if (success) {
220
+ const success = await copyFileWithRootReplacement(sourcePath, destPath, true, preserveExisting);
221
+ // Only count files actually written; 'preserved' (existing user-owned L3)
222
+ // and false (error) must not inflate the installed-files manifest.
223
+ if (success === true) {
201
224
  copiedFiles.push(path.relative(destDir, destPath));
202
225
  if (onProgress) {
203
226
  onProgress({ file: item.name, copied: true });
@@ -290,6 +313,7 @@ async function installSinapseCore(options = {}) {
290
313
  folderSource,
291
314
  folderDest,
292
315
  onProgress,
316
+ isUserOwnedL3,
293
317
  );
294
318
 
295
319
  if (copiedFiles.length > 0) {
@@ -306,8 +330,8 @@ async function installSinapseCore(options = {}) {
306
330
 
307
331
  if (await fs.pathExists(fileSource)) {
308
332
  spinner.text = `Copying ${file}...`;
309
- const success = await copyFileWithRootReplacement(fileSource, fileDest);
310
- if (success) {
333
+ const success = await copyFileWithRootReplacement(fileSource, fileDest, true, isUserOwnedL3);
334
+ if (success === true) {
311
335
  result.installedFiles.push(file);
312
336
  }
313
337
  }
@@ -495,6 +519,7 @@ module.exports = {
495
519
  getSinapseCoreSourcePath,
496
520
  copyFileWithRootReplacement,
497
521
  copyDirectoryWithRootReplacement,
522
+ isUserOwnedL3,
498
523
  generateVersionJson,
499
524
  generateFileHashes,
500
525
  deliverCodexRootFiles,
@@ -31,6 +31,29 @@ const path = require('path');
31
31
  const { collectCounts } = require('./sync-counts.js');
32
32
 
33
33
  const ROOT = path.resolve(__dirname, '..');
34
+ const CONSTITUTION = path.join(ROOT, '.sinapse-ai', 'constitution.md');
35
+
36
+ /**
37
+ * Count Constitution articles + NON-NEGOTIABLE ones from the canonical source.
38
+ * Articles are `### <Roman>. <Title>` headings; NON-NEGOTIABLE ones carry the
39
+ * `(NON-NEGOTIABLE)` marker in the heading. This is what was drifting in the
40
+ * README ("10 articles" while the constitution had 11) with no guard — now there
41
+ * is one.
42
+ * @returns {{articles:number, nonNegotiable:number}}
43
+ */
44
+ function collectConstitutionCounts() {
45
+ let articles = 0;
46
+ let nonNegotiable = 0;
47
+ try {
48
+ const md = fs.readFileSync(CONSTITUTION, 'utf8');
49
+ const headings = md.match(/^###\s+[IVXLC]+\.\s.*$/gm) || [];
50
+ articles = headings.length;
51
+ nonNegotiable = headings.filter((h) => /\(NON-NEGOTIABLE\)/.test(h)).length;
52
+ } catch {
53
+ // Missing/unreadable constitution → leave zeros; the guards below are no-ops.
54
+ }
55
+ return { articles, nonNegotiable };
56
+ }
34
57
 
35
58
  const TARGETS = [
36
59
  { file: 'README.md', label: 'README PT' },
@@ -44,12 +67,14 @@ const TARGETS = [
44
67
  * Build the set of expected numeric tokens that MUST appear consistently.
45
68
  * Returns an object describing canonical counts and a regex helper.
46
69
  */
47
- function buildExpectations(counts) {
70
+ function buildExpectations(counts, constitution = { articles: 0, nonNegotiable: 0 }) {
48
71
  return {
49
72
  squads: counts.squads,
50
73
  agents: counts.totalAgents,
51
74
  orqx: counts.totalOrqx,
52
75
  tasks: counts.tasks,
76
+ articles: constitution.articles,
77
+ nonNegotiable: constitution.nonNegotiable,
53
78
  };
54
79
  }
55
80
 
@@ -89,6 +114,39 @@ function findDrift(content, expected) {
89
114
  }
90
115
  }
91
116
 
117
+ // Constitution total articles: "<N> artigos"/"<N> articles" NOT immediately
118
+ // part of a NON-NEGOTIABLE clause, plus the shields.io badge
119
+ // "Constitution-<N>%20articles". Guards the exact drift that slipped through
120
+ // before (README said "10 articles" while the constitution had 11).
121
+ if (expected.articles) {
122
+ const artRe = /\b(\d{1,2})\s+(?:artigos|articles)\b(?![^.\n]{0,40}NON-NEGOTIABLE)/gi;
123
+ for (const m of content.matchAll(artRe)) {
124
+ const n = parseInt(m[1], 10);
125
+ if (n !== expected.articles && n >= 5) {
126
+ findings.push({ kind: 'articles', found: n, expected: expected.articles, snippet: m[0].trim() });
127
+ }
128
+ }
129
+ const badgeRe = /Constitution-(\d{1,2})%20articles/gi;
130
+ for (const m of content.matchAll(badgeRe)) {
131
+ const n = parseInt(m[1], 10);
132
+ if (n !== expected.articles) {
133
+ findings.push({ kind: 'articles (badge)', found: n, expected: expected.articles, snippet: m[0] });
134
+ }
135
+ }
136
+ }
137
+
138
+ // Constitution NON-NEGOTIABLE article count: "<N> (desses|of those) artigos
139
+ // sao/are NON-NEGOTIABLE".
140
+ if (expected.nonNegotiable) {
141
+ const nnRe = /\b(\d{1,2})\s+(?:(?:desses|de|of\s+those)\s+)?(?:artigos|articles)\s+(?:sao|são|are)\s+NON-NEGOTIABLE/gi;
142
+ for (const m of content.matchAll(nnRe)) {
143
+ const n = parseInt(m[1], 10);
144
+ if (n !== expected.nonNegotiable) {
145
+ findings.push({ kind: 'NON-NEGOTIABLE articles', found: n, expected: expected.nonNegotiable, snippet: m[0].trim() });
146
+ }
147
+ }
148
+ }
149
+
92
150
  return findings;
93
151
  }
94
152
 
@@ -112,14 +170,17 @@ function main() {
112
170
  const fixMode = args.includes('--fix');
113
171
 
114
172
  const counts = collectCounts();
115
- const expected = buildExpectations(counts);
173
+ const constitution = collectConstitutionCounts();
174
+ const expected = buildExpectations(counts, constitution);
116
175
 
117
176
  console.log('Article VII (Metrics Accuracy) — validacao de drift');
118
177
  console.log('Numeros canonicos (de .sinapse-ai/constitution.md):');
119
- console.log(` squads ${expected.squads}`);
120
- console.log(` agentes ${expected.agents}`);
121
- console.log(` orqx ${expected.orqx}`);
122
- console.log(` tasks ${expected.tasks}`);
178
+ console.log(` squads ${expected.squads}`);
179
+ console.log(` agentes ${expected.agents}`);
180
+ console.log(` orqx ${expected.orqx}`);
181
+ console.log(` tasks ${expected.tasks}`);
182
+ console.log(` artigos ${expected.articles}`);
183
+ console.log(` NON-NEGOTIABLE ${expected.nonNegotiable}`);
123
184
  console.log('');
124
185
 
125
186
  const allDrifts = [];
@@ -173,4 +234,4 @@ if (require.main === module) {
173
234
  main();
174
235
  }
175
236
 
176
- module.exports = { findDrift, buildExpectations, TARGETS };
237
+ module.exports = { findDrift, buildExpectations, collectConstitutionCounts, TARGETS };