@nforma.ai/nforma 0.2.1

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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * Scan a repository for signals indicating project intent.
9
+ * Returns suggested intent with confidence levels and confirmation needs.
10
+ *
11
+ * @param {string} rootPath - Path to project root
12
+ * @returns {Object} { suggested, signals, needs_confirmation }
13
+ */
14
+ function detectProjectIntent(rootPath) {
15
+ const root = rootPath || process.cwd();
16
+
17
+ const signals = [];
18
+ const suggested = {
19
+ base_profile: 'unknown',
20
+ iac: false,
21
+ deploy: 'none',
22
+ sensitive: false,
23
+ oss: false,
24
+ monorepo: false,
25
+ };
26
+
27
+ // Helper to check if file exists
28
+ const fileExists = (filePath) => {
29
+ try {
30
+ return fs.existsSync(path.join(root, filePath));
31
+ } catch (_) {
32
+ return false;
33
+ }
34
+ };
35
+
36
+ // Helper to check if any file matching a pattern exists
37
+ const globExists = (pattern) => {
38
+ try {
39
+ const dir = path.join(root, path.dirname(pattern));
40
+ if (!fs.existsSync(dir)) return false;
41
+ const filename = path.basename(pattern);
42
+ const files = fs.readdirSync(dir);
43
+ const regex = filename.replace(/\*/g, '.*');
44
+ return files.some(f => new RegExp(`^${regex}$`).test(f));
45
+ } catch (_) {
46
+ return false;
47
+ }
48
+ };
49
+
50
+ // Helper to read package.json
51
+ const getPackageJson = () => {
52
+ try {
53
+ if (fileExists('package.json')) {
54
+ const content = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
55
+ return JSON.parse(content);
56
+ }
57
+ } catch (_) {}
58
+ return null;
59
+ };
60
+
61
+ // ============================================================================
62
+ // base_profile detection
63
+ // ============================================================================
64
+
65
+ const pkg = getPackageJson();
66
+ let baseProfileFound = false;
67
+
68
+ if (pkg) {
69
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
70
+ const depsStr = JSON.stringify(deps);
71
+
72
+ // web frameworks
73
+ if (/next|nuxt|vite|gatsby|remix/.test(depsStr)) {
74
+ suggested.base_profile = 'web';
75
+ signals.push({
76
+ dimension: 'base_profile',
77
+ confidence: 'medium',
78
+ evidence: ['Web framework detected in package.json (next/nuxt/vite/gatsby/remix)'],
79
+ });
80
+ baseProfileFound = true;
81
+ }
82
+ // mobile frameworks
83
+ else if (/react-native|expo/.test(depsStr)) {
84
+ suggested.base_profile = 'mobile';
85
+ signals.push({
86
+ dimension: 'base_profile',
87
+ confidence: 'medium',
88
+ evidence: ['Mobile framework detected in package.json (react-native/expo)'],
89
+ });
90
+ baseProfileFound = true;
91
+ }
92
+ // desktop frameworks
93
+ else if (/electron|tauri/.test(depsStr)) {
94
+ suggested.base_profile = 'desktop';
95
+ signals.push({
96
+ dimension: 'base_profile',
97
+ confidence: 'medium',
98
+ evidence: ['Desktop framework detected in package.json (electron/tauri)'],
99
+ });
100
+ baseProfileFound = true;
101
+ }
102
+
103
+ // Check for bin field (CLI)
104
+ if (pkg.bin && !baseProfileFound) {
105
+ suggested.base_profile = 'cli';
106
+ signals.push({
107
+ dimension: 'base_profile',
108
+ confidence: 'medium',
109
+ evidence: ['bin field present in package.json'],
110
+ });
111
+ baseProfileFound = true;
112
+ }
113
+
114
+ // Check for OpenAPI (API)
115
+ if (!baseProfileFound && (fileExists('openapi.json') || fileExists('openapi.yaml') || fileExists('swagger.json'))) {
116
+ suggested.base_profile = 'api';
117
+ signals.push({
118
+ dimension: 'base_profile',
119
+ confidence: 'medium',
120
+ evidence: ['OpenAPI/Swagger spec found (openapi.json/openapi.yaml/swagger.json)'],
121
+ });
122
+ baseProfileFound = true;
123
+ }
124
+ }
125
+
126
+ if (!baseProfileFound) {
127
+ signals.push({
128
+ dimension: 'base_profile',
129
+ confidence: 'low',
130
+ evidence: ['No framework or project markers detected'],
131
+ });
132
+ }
133
+
134
+ // ============================================================================
135
+ // IaC detection
136
+ // ============================================================================
137
+
138
+ const iacSignals = [];
139
+
140
+ if (globExists('*.tf')) {
141
+ iacSignals.push('Terraform files (*.tf) found');
142
+ }
143
+ if (fileExists('terraform/main.tf')) {
144
+ iacSignals.push('terraform/main.tf found');
145
+ }
146
+ if (fileExists('Pulumi.yaml')) {
147
+ iacSignals.push('Pulumi.yaml found');
148
+ }
149
+ if (fileExists('cdk.json')) {
150
+ iacSignals.push('cdk.json (AWS CDK) found');
151
+ }
152
+ if (fileExists('serverless.yml')) {
153
+ iacSignals.push('serverless.yml found');
154
+ }
155
+ if (fileExists('infra/') && fs.statSync(path.join(root, 'infra')).isDirectory()) {
156
+ iacSignals.push('infra/ directory found');
157
+ }
158
+
159
+ if (iacSignals.length > 0) {
160
+ suggested.iac = true;
161
+ signals.push({
162
+ dimension: 'iac',
163
+ confidence: 'high',
164
+ evidence: iacSignals,
165
+ });
166
+ }
167
+
168
+ // ============================================================================
169
+ // deploy detection
170
+ // ============================================================================
171
+
172
+ const deploySignals = [];
173
+
174
+ if (fileExists('Dockerfile')) {
175
+ suggested.deploy = 'docker';
176
+ deploySignals.push('Dockerfile found');
177
+ } else if (fileExists('docker-compose.yml')) {
178
+ suggested.deploy = 'docker';
179
+ deploySignals.push('docker-compose.yml found');
180
+ } else if (fileExists('fly.toml')) {
181
+ suggested.deploy = 'fly';
182
+ deploySignals.push('fly.toml found');
183
+ } else if (fileExists('vercel.json')) {
184
+ suggested.deploy = 'vercel';
185
+ deploySignals.push('vercel.json found');
186
+ } else if (fileExists('Procfile')) {
187
+ suggested.deploy = 'heroku';
188
+ deploySignals.push('Procfile found');
189
+ }
190
+
191
+ if (deploySignals.length > 0) {
192
+ signals.push({
193
+ dimension: 'deploy',
194
+ confidence: 'high',
195
+ evidence: deploySignals,
196
+ });
197
+ }
198
+
199
+ // ============================================================================
200
+ // sensitive detection
201
+ // ============================================================================
202
+
203
+ const sensitiveSignals = [];
204
+
205
+ if (pkg) {
206
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
207
+ const depsStr = JSON.stringify(deps);
208
+
209
+ if (/passport|next-auth|auth0|firebase|okta/.test(depsStr)) {
210
+ sensitiveSignals.push('Auth library detected (passport/next-auth/auth0/firebase/okta)');
211
+ }
212
+ if (/stripe|paypal|square|shopify|commerce/.test(depsStr)) {
213
+ sensitiveSignals.push('Payment library detected (stripe/paypal/square/shopify)');
214
+ }
215
+ }
216
+
217
+ if (sensitiveSignals.length > 0) {
218
+ suggested.sensitive = true;
219
+ signals.push({
220
+ dimension: 'sensitive',
221
+ confidence: 'medium',
222
+ evidence: sensitiveSignals,
223
+ });
224
+ }
225
+
226
+ // ============================================================================
227
+ // oss detection
228
+ // ============================================================================
229
+
230
+ const ossSignals = [];
231
+
232
+ if (fileExists('LICENSE')) {
233
+ ossSignals.push('LICENSE file found');
234
+ }
235
+ if (fileExists('CONTRIBUTING.md')) {
236
+ ossSignals.push('CONTRIBUTING.md found');
237
+ }
238
+ if (fileExists('CODE_OF_CONDUCT.md')) {
239
+ ossSignals.push('CODE_OF_CONDUCT.md found');
240
+ }
241
+
242
+ if (ossSignals.length > 0) {
243
+ suggested.oss = true;
244
+ signals.push({
245
+ dimension: 'oss',
246
+ confidence: 'high',
247
+ evidence: ossSignals,
248
+ });
249
+ }
250
+
251
+ // ============================================================================
252
+ // monorepo detection
253
+ // ============================================================================
254
+
255
+ const monorepoSignals = [];
256
+
257
+ if (fileExists('pnpm-workspace.yaml')) {
258
+ monorepoSignals.push('pnpm-workspace.yaml found');
259
+ }
260
+ if (fileExists('lerna.json')) {
261
+ monorepoSignals.push('lerna.json found');
262
+ }
263
+ if (fileExists('nx.json')) {
264
+ monorepoSignals.push('nx.json found');
265
+ }
266
+ if (fileExists('turbo.json')) {
267
+ monorepoSignals.push('turbo.json found');
268
+ }
269
+
270
+ if (monorepoSignals.length > 0) {
271
+ suggested.monorepo = true;
272
+ signals.push({
273
+ dimension: 'monorepo',
274
+ confidence: 'high',
275
+ evidence: monorepoSignals,
276
+ });
277
+ }
278
+
279
+ // ============================================================================
280
+ // Build needs_confirmation array
281
+ // ============================================================================
282
+
283
+ const needsConfirmation = [];
284
+
285
+ // Medium-confidence dimensions need confirmation
286
+ for (const signal of signals) {
287
+ if (signal.confidence === 'medium') {
288
+ needsConfirmation.push(signal.dimension);
289
+ }
290
+ }
291
+
292
+ // Unknown base_profile always needs confirmation
293
+ if (suggested.base_profile === 'unknown') {
294
+ if (!needsConfirmation.includes('base_profile')) {
295
+ needsConfirmation.push('base_profile');
296
+ }
297
+ }
298
+
299
+ return {
300
+ suggested,
301
+ signals,
302
+ needs_confirmation: needsConfirmation,
303
+ };
304
+ }
305
+
306
+ // ============================================================================
307
+ // CLI mode
308
+ // ============================================================================
309
+
310
+ if (require.main === module) {
311
+ const args = process.argv.slice(2);
312
+
313
+ // Parse --root and --json flags
314
+ let rootPath = process.cwd();
315
+ let jsonOutput = false;
316
+
317
+ for (let i = 0; i < args.length; i++) {
318
+ if (args[i] === '--root' && args[i + 1]) {
319
+ rootPath = args[i + 1];
320
+ i++;
321
+ } else if (args[i] === '--json') {
322
+ jsonOutput = true;
323
+ }
324
+ }
325
+
326
+ try {
327
+ const result = detectProjectIntent(rootPath);
328
+
329
+ if (jsonOutput) {
330
+ console.log(JSON.stringify(result, null, 2));
331
+ } else {
332
+ // Human-readable output
333
+ console.log('Project Intent Detection\n');
334
+ console.log('Suggested Intent:');
335
+ console.log(` base_profile: ${result.suggested.base_profile}`);
336
+ console.log(` iac: ${result.suggested.iac}`);
337
+ console.log(` deploy: ${result.suggested.deploy}`);
338
+ console.log(` sensitive: ${result.suggested.sensitive}`);
339
+ console.log(` oss: ${result.suggested.oss}`);
340
+ console.log(` monorepo: ${result.suggested.monorepo}\n`);
341
+
342
+ if (result.signals.length > 0) {
343
+ console.log('Signals:\n');
344
+ for (const signal of result.signals) {
345
+ console.log(` ${signal.dimension} (${signal.confidence} confidence):`);
346
+ for (const evidence of signal.evidence) {
347
+ console.log(` - ${evidence}`);
348
+ }
349
+ }
350
+ }
351
+
352
+ if (result.needs_confirmation.length > 0) {
353
+ console.log(`\nNeeds Confirmation:\n ${result.needs_confirmation.join(', ')}`);
354
+ }
355
+ }
356
+ } catch (err) {
357
+ console.error(`Error detecting project intent: ${err.message}`);
358
+ process.exit(1);
359
+ }
360
+ }
361
+
362
+ module.exports = { detectProjectIntent };
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/export-prism-constants.cjs
4
+ // Reads .planning/quorum-scoreboard.json and exports per-slot TP/UNAVAIL rates
5
+ // as PRISM const declarations to .planning/formal/prism/rates.const.
6
+ // Requirements: PRM-02, PRM-03
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // ── Constants ────────────────────────────────────────────────────────────────
12
+ const MIN_ROUNDS = 30; // PRM-03: minimum rounds to use empirical rates
13
+ const TP_PRIOR = 0.85; // conservative TP prior when < 30 rounds
14
+ const UNAVAIL_PRIOR = 0.15; // conservative UNAVAIL prior when < 30 rounds
15
+
16
+ // Claude is excluded — Claude's self-vote is structural, not externally measured
17
+ const SLOTS = ['gemini', 'opencode', 'copilot', 'codex'];
18
+
19
+ // ── Pure Functions (exported for unit testing) ───────────────────────────────
20
+
21
+ /**
22
+ * Compute per-slot TP and UNAVAIL rates from scoreboard rounds.
23
+ *
24
+ * @param {Array} rounds - scoreboard.rounds array
25
+ * @param {string} slot - model slot name (e.g. 'gemini')
26
+ * @param {number} minRounds - minimum rounds threshold (PRM-03)
27
+ * @param {number} tpPrior - conservative TP prior
28
+ * @param {number} unavailPrior - conservative UNAVAIL prior
29
+ * @returns {{ n: number, tpRate: number, unavailRate: number, usedPrior: boolean, warning: string|null }}
30
+ */
31
+ function computeSlotRates(rounds, slot, minRounds, tpPrior, unavailPrior) {
32
+ // Filter to rounds where this slot has a classifiable vote.
33
+ // Exclude Mode A empty-string results and the UNAVAILABLE typo variant —
34
+ // they carry no binary signal and would inflate apparent unavailability.
35
+ const participated = rounds.filter(r => {
36
+ const v = r.votes && r.votes[slot];
37
+ return v !== undefined && v !== '' && v !== 'UNAVAILABLE';
38
+ });
39
+ const n = participated.length;
40
+
41
+ if (n < minRounds) {
42
+ return {
43
+ n,
44
+ tpRate: tpPrior,
45
+ unavailRate: unavailPrior,
46
+ usedPrior: true,
47
+ warning: `WARNING: slot '${slot}' has only ${n} rounds (fewer than ${minRounds} threshold) — using conservative priors (${tpPrior}/${unavailPrior})`,
48
+ };
49
+ }
50
+
51
+ // Count APPROVE (TP, TP+) and UNAVAIL votes
52
+ let approveCount = 0;
53
+ let unavailCount = 0;
54
+ for (const r of participated) {
55
+ const vote = r.votes[slot];
56
+ if (vote === 'TP' || vote === 'TP+') {
57
+ approveCount++;
58
+ } else if (vote === 'UNAVAIL') {
59
+ unavailCount++;
60
+ }
61
+ }
62
+
63
+ const tpRate = n > 0 ? approveCount / n : tpPrior;
64
+ const unavailRate = n > 0 ? unavailCount / n : unavailPrior;
65
+
66
+ return {
67
+ n,
68
+ tpRate: parseFloat(tpRate.toFixed(4)),
69
+ unavailRate: parseFloat(unavailRate.toFixed(4)),
70
+ usedPrior: false,
71
+ warning: null,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Build PRISM const declaration lines for a slot.
77
+ *
78
+ * @param {string} slot - model slot name
79
+ * @param {{ n: number, tpRate: number, unavailRate: number, usedPrior: boolean }} rateResult
80
+ * @returns {string[]} - array of string lines (without trailing newlines)
81
+ */
82
+ function buildConstLines(slot, rateResult) {
83
+ const { n, tpRate, unavailRate, usedPrior } = rateResult;
84
+ const label = usedPrior
85
+ ? `// ${slot}: n=${n} rounds — using priors (< ${MIN_ROUNDS} threshold)`
86
+ : `// ${slot}: n=${n} rounds (empirical)`;
87
+ return [
88
+ label,
89
+ `const double tp_${slot} = ${tpRate};`,
90
+ `const double unavail_${slot} = ${unavailRate};`,
91
+ '',
92
+ ];
93
+ }
94
+
95
+ // ── Export pure functions for unit testing ───────────────────────────────────
96
+ module.exports._pure = { computeSlotRates, buildConstLines };
97
+
98
+ // ── Main ─────────────────────────────────────────────────────────────────────
99
+ // Guard against running main logic when required as a module (test imports)
100
+ if (require.main === module) {
101
+ let scoreboardPath;
102
+ try {
103
+ const pp = require('./planning-paths.cjs');
104
+ scoreboardPath = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
105
+ } catch (_) {
106
+ scoreboardPath = path.join(process.cwd(), '.planning', 'quorum-scoreboard.json');
107
+ }
108
+ const outputPath = path.join(process.cwd(), '.planning', 'formal', 'prism', 'rates.const');
109
+
110
+ // ── Locate scoreboard ──────────────────────────────────────────────────────
111
+ if (!fs.existsSync(scoreboardPath)) {
112
+ process.stderr.write(
113
+ '[export-prism-constants] No scoreboard file found at: ' + scoreboardPath + '\n' +
114
+ '[export-prism-constants] Run quorum rounds to populate the scoreboard first.\n'
115
+ );
116
+ process.exit(1);
117
+ }
118
+
119
+ let scoreboard;
120
+ try {
121
+ scoreboard = JSON.parse(fs.readFileSync(scoreboardPath, 'utf8'));
122
+ } catch (err) {
123
+ process.stderr.write(
124
+ '[export-prism-constants] Failed to parse scoreboard: ' + err.message + '\n'
125
+ );
126
+ process.exit(1);
127
+ }
128
+
129
+ const rounds = scoreboard.rounds || [];
130
+
131
+ // ── Compute rates per slot ─────────────────────────────────────────────────
132
+ const outputLines = [
133
+ '// PRISM rate constants — generated by bin/export-prism-constants.cjs',
134
+ '// Source: .planning/quorum-scoreboard.json (' + rounds.length + ' total rounds)',
135
+ '// Paste these lines into .planning/formal/prism/quorum.pm to override the default priors.',
136
+ '// Or use each as a -const CLI flag when invoking PRISM.',
137
+ '',
138
+ ];
139
+
140
+ let hasWarnings = false;
141
+
142
+ for (const slot of SLOTS) {
143
+ const rateResult = computeSlotRates(rounds, slot, MIN_ROUNDS, TP_PRIOR, UNAVAIL_PRIOR);
144
+
145
+ if (rateResult.warning) {
146
+ process.stderr.write('[export-prism-constants] ' + rateResult.warning + '\n');
147
+ hasWarnings = true;
148
+ }
149
+
150
+ const lines = buildConstLines(slot, rateResult);
151
+ outputLines.push(...lines);
152
+ }
153
+
154
+ // ── Write rates.const ─────────────────────────────────────────────────────
155
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
156
+ fs.writeFileSync(outputPath, outputLines.join('\n') + '\n');
157
+
158
+ process.stdout.write('[export-prism-constants] Written: ' + outputPath + '\n');
159
+ if (hasWarnings) {
160
+ process.stdout.write('[export-prism-constants] Note: some slots used conservative priors (< ' + MIN_ROUNDS + ' rounds)\n');
161
+ }
162
+
163
+ process.exit(0);
164
+ }