@nforma.ai/nforma 0.2.1 → 0.28.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 (201) hide show
  1. package/README.md +2 -2
  2. package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
  3. package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
  4. package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
  5. package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
  6. package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
  7. package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
  8. package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
  9. package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
  10. package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
  11. package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
  12. package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
  13. package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
  14. package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
  15. package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
  16. package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
  17. package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
  18. package/bin/accept-debug-invariant.cjs +2 -2
  19. package/bin/account-manager.cjs +10 -10
  20. package/bin/aggregate-requirements.cjs +1 -1
  21. package/bin/analyze-assumptions.cjs +3 -3
  22. package/bin/analyze-state-space.cjs +14 -14
  23. package/bin/assumption-register.cjs +146 -0
  24. package/bin/attribute-trace-divergence.cjs +1 -1
  25. package/bin/auth-drivers/gh-cli.cjs +1 -1
  26. package/bin/auth-drivers/pool.cjs +1 -1
  27. package/bin/autoClosePtoF.cjs +3 -3
  28. package/bin/budget-tracker.cjs +77 -0
  29. package/bin/build-layer-manifest.cjs +153 -0
  30. package/bin/call-quorum-slot.cjs +3 -3
  31. package/bin/ccr-secure-config.cjs +5 -5
  32. package/bin/check-bundled-sdks.cjs +1 -1
  33. package/bin/check-mcp-health.cjs +1 -1
  34. package/bin/check-provider-health.cjs +6 -6
  35. package/bin/check-spec-sync.cjs +26 -26
  36. package/bin/check-trace-schema-drift.cjs +5 -5
  37. package/bin/conformance-schema.cjs +2 -2
  38. package/bin/cross-layer-dashboard.cjs +297 -0
  39. package/bin/design-impact.cjs +377 -0
  40. package/bin/detect-coverage-gaps.cjs +7 -7
  41. package/bin/failure-mode-catalog.cjs +227 -0
  42. package/bin/failure-taxonomy.cjs +177 -0
  43. package/bin/formal-scope-scan.cjs +179 -0
  44. package/bin/gate-a-grounding.cjs +334 -0
  45. package/bin/gate-b-abstraction.cjs +243 -0
  46. package/bin/gate-c-validation.cjs +166 -0
  47. package/bin/generate-formal-specs.cjs +17 -17
  48. package/bin/generate-petri-net.cjs +3 -3
  49. package/bin/generate-tla-cfg.cjs +5 -5
  50. package/bin/git-heatmap.cjs +571 -0
  51. package/bin/harness-diagnostic.cjs +326 -0
  52. package/bin/hazard-model.cjs +261 -0
  53. package/bin/install-formal-tools.cjs +1 -1
  54. package/bin/install.js +184 -139
  55. package/bin/instrumentation-map.cjs +178 -0
  56. package/bin/invariant-catalog.cjs +437 -0
  57. package/bin/issue-classifier.cjs +2 -2
  58. package/bin/load-baseline-requirements.cjs +4 -4
  59. package/bin/manage-agents-core.cjs +32 -32
  60. package/bin/migrate-to-slots.cjs +39 -39
  61. package/bin/mismatch-register.cjs +217 -0
  62. package/bin/nForma.cjs +176 -81
  63. package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
  64. package/bin/observe-config.cjs +8 -0
  65. package/bin/observe-debt-writer.cjs +1 -1
  66. package/bin/observe-handler-deps.cjs +356 -0
  67. package/bin/observe-handler-grafana.cjs +2 -17
  68. package/bin/observe-handler-internal.cjs +5 -5
  69. package/bin/observe-handler-logstash.cjs +2 -17
  70. package/bin/observe-handler-prometheus.cjs +2 -17
  71. package/bin/observe-handler-upstream.cjs +251 -0
  72. package/bin/observe-handlers.cjs +12 -33
  73. package/bin/observe-render.cjs +68 -22
  74. package/bin/observe-utils.cjs +37 -0
  75. package/bin/observed-fsm.cjs +324 -0
  76. package/bin/planning-paths.cjs +6 -0
  77. package/bin/polyrepo.cjs +1 -1
  78. package/bin/probe-quorum-slots.cjs +1 -1
  79. package/bin/promote-gate-maturity.cjs +274 -0
  80. package/bin/promote-model.cjs +1 -1
  81. package/bin/propose-debug-invariants.cjs +1 -1
  82. package/bin/quorum-cache.cjs +144 -0
  83. package/bin/quorum-consensus-gate.cjs +1 -1
  84. package/bin/quorum-slot-dispatch.cjs +6 -6
  85. package/bin/requirements-core.cjs +1 -1
  86. package/bin/review-mcp-logs.cjs +1 -1
  87. package/bin/risk-heatmap.cjs +151 -0
  88. package/bin/run-account-manager-tlc.cjs +4 -4
  89. package/bin/run-account-pool-alloy.cjs +2 -2
  90. package/bin/run-alloy.cjs +2 -2
  91. package/bin/run-audit-alloy.cjs +2 -2
  92. package/bin/run-breaker-tlc.cjs +3 -3
  93. package/bin/run-formal-check.cjs +9 -9
  94. package/bin/run-formal-verify.cjs +30 -9
  95. package/bin/run-installer-alloy.cjs +2 -2
  96. package/bin/run-oscillation-tlc.cjs +4 -4
  97. package/bin/run-phase-tlc.cjs +1 -1
  98. package/bin/run-protocol-tlc.cjs +4 -4
  99. package/bin/run-quorum-composition-alloy.cjs +2 -2
  100. package/bin/run-sensitivity-sweep.cjs +2 -2
  101. package/bin/run-stop-hook-tlc.cjs +3 -3
  102. package/bin/run-tlc.cjs +21 -21
  103. package/bin/run-transcript-alloy.cjs +2 -2
  104. package/bin/secrets.cjs +5 -5
  105. package/bin/security-sweep.cjs +238 -0
  106. package/bin/sensitivity-report.cjs +3 -3
  107. package/bin/set-secret.cjs +5 -5
  108. package/bin/setup-telemetry-cron.sh +3 -3
  109. package/bin/stall-detector.cjs +126 -0
  110. package/bin/state-candidates.cjs +206 -0
  111. package/bin/sync-baseline-requirements.cjs +1 -1
  112. package/bin/telemetry-collector.cjs +1 -1
  113. package/bin/test-changed.cjs +111 -0
  114. package/bin/test-recipe-gen.cjs +250 -0
  115. package/bin/trace-corpus-stats.cjs +211 -0
  116. package/bin/unified-mcp-server.mjs +3 -3
  117. package/bin/update-scoreboard.cjs +1 -1
  118. package/bin/validate-memory.cjs +2 -2
  119. package/bin/validate-traces.cjs +10 -10
  120. package/bin/verify-quorum-health.cjs +66 -5
  121. package/bin/xstate-to-tla.cjs +4 -4
  122. package/bin/xstate-trace-walker.cjs +3 -3
  123. package/commands/{qgsd → nf}/add-phase.md +3 -3
  124. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  125. package/commands/{qgsd → nf}/add-todo.md +3 -3
  126. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  127. package/commands/{qgsd → nf}/check-todos.md +3 -3
  128. package/commands/{qgsd → nf}/cleanup.md +3 -3
  129. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  130. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  131. package/commands/{qgsd → nf}/debug.md +9 -9
  132. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  133. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  134. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  135. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  136. package/commands/{qgsd → nf}/health.md +3 -3
  137. package/commands/{qgsd → nf}/help.md +3 -3
  138. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  139. package/commands/nf/join-discord.md +18 -0
  140. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  141. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  142. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  143. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  145. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  146. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  147. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  148. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  149. package/commands/{qgsd → nf}/new-project.md +8 -8
  150. package/commands/{qgsd → nf}/observe.md +49 -16
  151. package/commands/{qgsd → nf}/pause-work.md +3 -3
  152. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  153. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  154. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  155. package/commands/{qgsd → nf}/progress.md +3 -3
  156. package/commands/{qgsd → nf}/queue.md +2 -2
  157. package/commands/{qgsd → nf}/quick.md +8 -8
  158. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  159. package/commands/{qgsd → nf}/quorum.md +40 -40
  160. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  161. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  162. package/commands/{qgsd → nf}/research-phase.md +12 -12
  163. package/commands/{qgsd → nf}/resume-work.md +3 -3
  164. package/commands/nf/review-requirements.md +31 -0
  165. package/commands/{qgsd → nf}/set-profile.md +3 -3
  166. package/commands/{qgsd → nf}/settings.md +6 -6
  167. package/commands/{qgsd → nf}/solve.md +35 -35
  168. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  169. package/commands/{qgsd → nf}/triage.md +10 -10
  170. package/commands/{qgsd → nf}/update.md +3 -3
  171. package/commands/{qgsd → nf}/verify-work.md +5 -5
  172. package/hooks/dist/config-loader.js +188 -32
  173. package/hooks/dist/conformance-schema.cjs +2 -2
  174. package/hooks/dist/gsd-context-monitor.js +118 -13
  175. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  176. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  177. package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/nf-precompact.test.js +227 -0
  180. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  181. package/hooks/dist/nf-prompt.test.js +698 -0
  182. package/hooks/dist/nf-session-start.js +185 -0
  183. package/hooks/dist/nf-session-start.test.js +354 -0
  184. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  185. package/hooks/dist/nf-slot-correlator.test.js +85 -0
  186. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  187. package/hooks/dist/nf-spec-regen.test.js +73 -0
  188. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  189. package/hooks/dist/nf-statusline.test.js +157 -0
  190. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  191. package/hooks/dist/nf-stop.test.js +1388 -0
  192. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  193. package/hooks/dist/nf-token-collector.test.js +262 -0
  194. package/hooks/dist/unified-mcp-server.mjs +2 -2
  195. package/package.json +4 -4
  196. package/scripts/build-hooks.js +13 -6
  197. package/scripts/secret-audit.sh +1 -1
  198. package/scripts/verify-hooks-sync.cjs +90 -0
  199. package/templates/{qgsd.json → nf.json} +4 -4
  200. package/commands/qgsd/join-discord.md +0 -18
  201. package/hooks/dist/qgsd-session-start.js +0 -122
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Upstream tracking handler for /nf:observe
3
+ * Fetches releases (and notable PRs for loose coupling) from upstream repos via gh CLI
4
+ *
5
+ * Coupling modes:
6
+ * tight — All releases since last check (evaluate for cherry-pick — our code may already be better)
7
+ * loose — Releases + notable merged PRs (inspirational — patterns, features, hardening)
8
+ *
9
+ * State persisted in .planning/upstream-state.json to track last-checked date per upstream
10
+ */
11
+
12
+ const { execFileSync } = require('node:child_process');
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+ const { parseDuration, formatAge } = require('./observe-utils.cjs');
16
+
17
+ const STATE_FILE = '.planning/upstream-state.json';
18
+
19
+ // Keywords that signal an inspirational change worth surfacing (loose coupling)
20
+ const INSPIRATION_KEYWORDS = [
21
+ 'feat', 'feature', 'pattern', 'harden', 'security', 'perf', 'refactor',
22
+ 'breaking', 'architecture', 'plugin', 'hook', 'workflow', 'agent'
23
+ ];
24
+
25
+ /**
26
+ * Load upstream state (last-checked timestamps per repo)
27
+ * @param {string} [basePath]
28
+ * @returns {object} { [repo]: { last_checked: ISO8601, last_release_tag: string } }
29
+ */
30
+ function loadUpstreamState(basePath) {
31
+ const stateFile = path.resolve(basePath || process.cwd(), STATE_FILE);
32
+ try {
33
+ return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Save upstream state
41
+ * @param {object} state
42
+ * @param {string} [basePath]
43
+ */
44
+ function saveUpstreamState(state, basePath) {
45
+ const stateFile = path.resolve(basePath || process.cwd(), STATE_FILE);
46
+ const dir = path.dirname(stateFile);
47
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
48
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n');
49
+ }
50
+
51
+ /**
52
+ * Fetch releases from upstream repo since a cutoff date
53
+ * @param {string} repo - owner/repo
54
+ * @param {string} since - ISO8601 cutoff
55
+ * @param {number} limit
56
+ * @param {Function} execFn
57
+ * @returns {Array} releases
58
+ */
59
+ function fetchReleases(repo, since, limit, execFn) {
60
+ const execFile = execFn || execFileSync;
61
+ try {
62
+ const output = execFile('gh', [
63
+ 'release', 'list', '--repo', repo,
64
+ '--limit', String(limit),
65
+ '--json', 'tagName,name,publishedAt,isPrerelease,url'
66
+ ], { encoding: 'utf8' });
67
+ let releases = JSON.parse(output);
68
+ if (since) {
69
+ const cutoff = new Date(since).getTime();
70
+ releases = releases.filter(r => new Date(r.publishedAt).getTime() > cutoff);
71
+ }
72
+ return releases;
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Fetch notable merged PRs from upstream repo (for loose/inspirational coupling)
80
+ * Filters by keyword matches in title or size threshold
81
+ * @param {string} repo
82
+ * @param {string} since - ISO8601 cutoff
83
+ * @param {number} limit
84
+ * @param {Function} execFn
85
+ * @returns {Array} PRs
86
+ */
87
+ function fetchNotablePRs(repo, since, limit, execFn) {
88
+ const execFile = execFn || execFileSync;
89
+ try {
90
+ const output = execFile('gh', [
91
+ 'pr', 'list', '--repo', repo,
92
+ '--state', 'merged',
93
+ '--limit', String(limit * 3), // fetch more, filter down
94
+ '--json', 'number,title,url,mergedAt,changedFiles,additions,deletions,labels'
95
+ ], { encoding: 'utf8' });
96
+ let prs = JSON.parse(output);
97
+
98
+ // Filter by date
99
+ if (since) {
100
+ const cutoff = new Date(since).getTime();
101
+ prs = prs.filter(pr => pr.mergedAt && new Date(pr.mergedAt).getTime() > cutoff);
102
+ }
103
+
104
+ // Filter for "notable" PRs — keyword match OR substantial size
105
+ prs = prs.filter(pr => {
106
+ const title = (pr.title || '').toLowerCase();
107
+ const hasKeyword = INSPIRATION_KEYWORDS.some(kw => title.includes(kw));
108
+ const isSubstantial = (pr.changedFiles || 0) >= 5 || (pr.additions || 0) >= 100;
109
+ return hasKeyword || isSubstantial;
110
+ });
111
+
112
+ return prs.slice(0, limit);
113
+ } catch {
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Classify severity/interest level from release or PR
120
+ * @param {object} item - release or PR object
121
+ * @param {string} itemType - 'release' or 'pr'
122
+ * @returns {string} severity string
123
+ */
124
+ function classifyUpstreamSeverity(item, itemType) {
125
+ if (itemType === 'release') {
126
+ const name = ((item.name || '') + ' ' + (item.tagName || '')).toLowerCase();
127
+ if (name.includes('breaking') || /\d+\.0\.0/.test(item.tagName || '')) return 'warning';
128
+ if (item.isPrerelease) return 'info';
129
+ return 'info';
130
+ }
131
+ // PR
132
+ const title = (item.title || '').toLowerCase();
133
+ if (title.includes('breaking') || title.includes('security') || title.includes('harden')) return 'warning';
134
+ if (title.includes('feat') || title.includes('pattern') || title.includes('refactor')) return 'info';
135
+ return 'info';
136
+ }
137
+
138
+ /**
139
+ * Upstream source handler
140
+ * @param {object} sourceConfig - { type, label, repo, coupling, branch?, filter?: { since } }
141
+ * @param {object} options - { sinceOverride?, limitOverride?, execFn?, basePath? }
142
+ * @returns {object} Standard observe schema result
143
+ */
144
+ function handleUpstream(sourceConfig, options) {
145
+ const label = sourceConfig.label || 'Upstream';
146
+ const execFile = options.execFn || execFileSync;
147
+ const basePath = options.basePath || process.cwd();
148
+ const coupling = sourceConfig.coupling || 'loose';
149
+ const repo = sourceConfig.repo;
150
+
151
+ if (!repo) {
152
+ return {
153
+ source_label: label,
154
+ source_type: 'upstream',
155
+ status: 'error',
156
+ error: 'No repo configured for upstream source',
157
+ issues: []
158
+ };
159
+ }
160
+
161
+ try {
162
+ // Determine cutoff date
163
+ const state = loadUpstreamState(basePath);
164
+ const repoState = state[repo] || {};
165
+ const filter = sourceConfig.filter || {};
166
+ const sinceOverride = options.sinceOverride || filter.since;
167
+
168
+ let since;
169
+ if (repoState.last_checked) {
170
+ since = repoState.last_checked;
171
+ } else if (sinceOverride) {
172
+ const ms = parseDuration(sinceOverride);
173
+ since = ms > 0 ? new Date(Date.now() - ms).toISOString() : null;
174
+ } else {
175
+ // Default: 14 days for first run
176
+ since = new Date(Date.now() - 14 * 86400000).toISOString();
177
+ }
178
+
179
+ const limit = options.limitOverride || filter.limit || 10;
180
+ const issues = [];
181
+
182
+ // Both tight and loose get releases
183
+ const releases = fetchReleases(repo, since, limit, execFile);
184
+ for (const rel of releases) {
185
+ issues.push({
186
+ id: `upstream-rel-${repo}-${rel.tagName}`,
187
+ title: `[${coupling === 'tight' ? 'Evaluate' : 'Inspiration'}] ${rel.name || rel.tagName}`,
188
+ severity: classifyUpstreamSeverity(rel, 'release'),
189
+ url: rel.url || `https://github.com/${repo}/releases/tag/${rel.tagName}`,
190
+ age: formatAge(rel.publishedAt),
191
+ created_at: rel.publishedAt || new Date().toISOString(),
192
+ meta: `${repo} ${rel.tagName}${rel.isPrerelease ? ' (pre-release)' : ''}`,
193
+ source_type: 'upstream',
194
+ issue_type: 'upstream',
195
+ _upstream: { coupling, repo, tag: rel.tagName }
196
+ });
197
+ }
198
+
199
+ // Loose coupling: also fetch notable merged PRs
200
+ if (coupling === 'loose') {
201
+ const prs = fetchNotablePRs(repo, since, limit, execFile);
202
+ for (const pr of prs) {
203
+ issues.push({
204
+ id: `upstream-pr-${repo}-${pr.number}`,
205
+ title: `[Inspiration] ${pr.title}`,
206
+ severity: classifyUpstreamSeverity(pr, 'pr'),
207
+ url: pr.url || `https://github.com/${repo}/pull/${pr.number}`,
208
+ age: formatAge(pr.mergedAt),
209
+ created_at: pr.mergedAt || new Date().toISOString(),
210
+ meta: `${repo} #${pr.number} (+${pr.additions || 0}/-${pr.deletions || 0}, ${pr.changedFiles || 0} files)`,
211
+ source_type: 'upstream',
212
+ issue_type: 'upstream',
213
+ _upstream: { coupling, repo, pr: pr.number }
214
+ });
215
+ }
216
+ }
217
+
218
+ // Update state
219
+ state[repo] = {
220
+ last_checked: new Date().toISOString(),
221
+ last_release_tag: releases.length > 0 ? releases[0].tagName : (repoState.last_release_tag || null),
222
+ coupling
223
+ };
224
+ saveUpstreamState(state, basePath);
225
+
226
+ return {
227
+ source_label: label,
228
+ source_type: 'upstream',
229
+ status: 'ok',
230
+ issues
231
+ };
232
+ } catch (err) {
233
+ return {
234
+ source_label: label,
235
+ source_type: 'upstream',
236
+ status: 'error',
237
+ error: `Upstream fetch failed: ${err.message}`,
238
+ issues: []
239
+ };
240
+ }
241
+ }
242
+
243
+ module.exports = {
244
+ handleUpstream,
245
+ loadUpstreamState,
246
+ saveUpstreamState,
247
+ fetchReleases,
248
+ fetchNotablePRs,
249
+ classifyUpstreamSeverity,
250
+ INSPIRATION_KEYWORDS
251
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Source handler implementations for /qgsd:observe
2
+ * Source handler implementations for /nf:observe
3
3
  * GitHub, Sentry, sentry-feedback, and bash handlers
4
4
  *
5
5
  * ALL handlers return the SAME schema:
@@ -10,42 +10,11 @@
10
10
  */
11
11
 
12
12
  const { execFileSync } = require('node:child_process');
13
+ const { parseDuration, formatAge } = require('./observe-utils.cjs');
13
14
 
14
15
  // Severity labels recognized from GitHub labels (ordered by priority)
15
16
  const SEVERITY_LABELS = ['critical', 'error', 'bug', 'warning', 'enhancement', 'info'];
16
17
 
17
- /**
18
- * Parse a duration string like "7d", "24h", "30m" into milliseconds
19
- * @param {string} duration - Duration string
20
- * @returns {number} Milliseconds
21
- */
22
- function parseDuration(duration) {
23
- if (!duration) return 0;
24
- const match = String(duration).match(/^(\d+)([dhms])$/);
25
- if (!match) return 0;
26
- const num = parseInt(match[1], 10);
27
- const unit = match[2];
28
- const multipliers = { d: 86400000, h: 3600000, m: 60000, s: 1000 };
29
- return num * (multipliers[unit] || 0);
30
- }
31
-
32
- /**
33
- * Format age from ISO date to human-readable string
34
- * @param {string} isoDate - ISO8601 date string
35
- * @returns {string} Human-readable age like "5m", "2h", "3d"
36
- */
37
- function formatAge(isoDate) {
38
- if (!isoDate) return 'unknown';
39
- const diffMs = Date.now() - new Date(isoDate).getTime();
40
- if (diffMs < 0) return 'future';
41
- const minutes = Math.floor(diffMs / 60000);
42
- if (minutes < 60) return `${minutes}m`;
43
- const hours = Math.floor(minutes / 60);
44
- if (hours < 24) return `${hours}h`;
45
- const days = Math.floor(hours / 24);
46
- return `${days}d`;
47
- }
48
-
49
18
  /**
50
19
  * Classify severity from GitHub labels
51
20
  * @param {Array} labels - Array of label objects with 'name' field, or strings
@@ -415,6 +384,12 @@ const { handleLogstash } = require('./observe-handler-logstash.cjs');
415
384
  // Internal work detection handler
416
385
  const { handleInternal } = require('./observe-handler-internal.cjs');
417
386
 
387
+ // Upstream tracking handler
388
+ const { handleUpstream } = require('./observe-handler-upstream.cjs');
389
+
390
+ // Dependency freshness handler
391
+ const { handleDeps } = require('./observe-handler-deps.cjs');
392
+
418
393
  module.exports = {
419
394
  handleGitHub,
420
395
  handleSentry,
@@ -428,6 +403,10 @@ module.exports = {
428
403
  handleLogstash,
429
404
  // Internal work detection (quick-168)
430
405
  handleInternal,
406
+ // Upstream tracking
407
+ handleUpstream,
408
+ // Dependency freshness
409
+ handleDeps,
431
410
  // Exported for testing
432
411
  parseDuration,
433
412
  formatAge,
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Dual-table renderer for /qgsd:observe
2
+ * Dual-table renderer for /nf:observe
3
3
  * Renders Issues table and Drifts table with error section
4
4
  */
5
5
 
6
+ const { formatAge } = require('./observe-utils.cjs');
7
+
6
8
  // Severity sort order (lower = higher priority)
7
9
  const SEVERITY_ORDER = { error: 0, critical: 0, bug: 1, warning: 2, info: 3 };
8
10
 
@@ -15,23 +17,6 @@ function classifySeverity(severity) {
15
17
  return SEVERITY_ORDER[severity] ?? 4;
16
18
  }
17
19
 
18
- /**
19
- * Format age from ISO date to human-readable string
20
- * @param {string} isoDate
21
- * @returns {string}
22
- */
23
- function formatAge(isoDate) {
24
- if (!isoDate) return '';
25
- const diffMs = Date.now() - new Date(isoDate).getTime();
26
- if (diffMs < 0) return 'future';
27
- const minutes = Math.floor(diffMs / 60000);
28
- if (minutes < 60) return `${minutes}m`;
29
- const hours = Math.floor(minutes / 60);
30
- if (hours < 24) return `${hours}h`;
31
- const days = Math.floor(hours / 24);
32
- return `${days}d`;
33
- }
34
-
35
20
  /**
36
21
  * Truncate a string to maxLen, adding "..." if truncated
37
22
  * @param {string} str
@@ -79,26 +64,32 @@ function renderObserveOutput(results) {
79
64
  }
80
65
 
81
66
  // Split by issue_type
82
- const issues = allItems.filter(item => item.issue_type !== 'drift');
67
+ const issues = allItems.filter(item => !['drift', 'upstream', 'deps'].includes(item.issue_type));
83
68
  const drifts = allItems.filter(item => item.issue_type === 'drift');
69
+ const upstreams = allItems.filter(item => item.issue_type === 'upstream');
70
+ const deps = allItems.filter(item => item.issue_type === 'deps');
84
71
 
85
72
  const totalIssues = issues.length;
86
73
  const totalDrifts = drifts.length;
74
+ const totalUpstreams = upstreams.length;
75
+ const totalDeps = deps.length;
87
76
 
88
77
  // Header
89
- if (totalIssues === 0 && totalDrifts === 0 && errorResults.length === 0) {
78
+ if (totalIssues === 0 && totalDrifts === 0 && totalUpstreams === 0 && totalDeps === 0 && errorResults.length === 0) {
90
79
  lines.push('');
91
80
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
92
- lines.push(' QGSD > OBSERVE: All clear — no open issues found');
81
+ lines.push(' nForma > OBSERVE: All clear — no open issues found');
93
82
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
94
83
  lines.push(`Sources checked: ${sourceCount}`);
95
84
  return lines.join('\n');
96
85
  }
97
86
 
87
+ const upstreamNote = totalUpstreams > 0 ? `, ${totalUpstreams} upstream(s)` : '';
88
+ const depsNote = totalDeps > 0 ? `, ${totalDeps} dep(s)` : '';
98
89
  const failNote = errorResults.length > 0 ? `; ${errorResults.length} source(s) failed` : '';
99
90
  lines.push('');
100
91
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
101
- lines.push(` QGSD > OBSERVE: ${totalIssues} issue(s), ${totalDrifts} drift(s) across ${sourceCount} source(s)${failNote}`);
92
+ lines.push(` nForma > OBSERVE: ${totalIssues} issue(s), ${totalDrifts} drift(s)${upstreamNote}${depsNote} across ${sourceCount} source(s)${failNote}`);
102
93
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
103
94
 
104
95
  // Sort issues by severity then age (newest first)
@@ -153,6 +144,61 @@ function renderObserveOutput(results) {
153
144
  lines.push('└─────────────────────────────────────────────────────────────────────┘');
154
145
  }
155
146
 
147
+ // Render Upstream table
148
+ if (upstreams.length > 0) {
149
+ // Sort: warnings first, then by age (newest first)
150
+ upstreams.sort((a, b) => {
151
+ const sevCmp = classifySeverity(a.severity) - classifySeverity(b.severity);
152
+ if (sevCmp !== 0) return sevCmp;
153
+ return new Date(b.created_at || 0) - new Date(a.created_at || 0);
154
+ });
155
+
156
+ lines.push('');
157
+ lines.push('┌──────────────────────────── UPSTREAM ──────────────────────────────┐');
158
+ lines.push('│ # │ Title │ Repo │ Tag │ Age │');
159
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
160
+
161
+ for (let i = 0; i < upstreams.length; i++) {
162
+ const item = upstreams[i];
163
+ const num = String(i + 1).padStart(2, ' ');
164
+ const title = pad(truncate(item.title, 40), 40);
165
+ const repo = pad(truncate((item._upstream?.repo || item.source_label || '').split('/').pop(), 7), 7);
166
+ const tag = pad(truncate(item._upstream?.tag || `#${item._upstream?.pr || ''}`, 3), 3);
167
+ const age = pad(item.age || formatAge(item.created_at), 4);
168
+ lines.push(`│ ${num} │ ${title} │ ${repo} │ ${tag} │ ${age} │`);
169
+ }
170
+
171
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
172
+ }
173
+
174
+ // Render Dependencies table
175
+ if (deps.length > 0) {
176
+ // Sort: vulns first (error), then warnings (major), then info (minor/patch)
177
+ deps.sort((a, b) => {
178
+ const sevCmp = classifySeverity(a.severity) - classifySeverity(b.severity);
179
+ if (sevCmp !== 0) return sevCmp;
180
+ // Within same severity, sort by package name
181
+ return (a.title || '').localeCompare(b.title || '');
182
+ });
183
+
184
+ lines.push('');
185
+ lines.push('┌────────────────────────── DEPENDENCIES ────────────────────────────┐');
186
+ lines.push('│ # │ Package │ Bump │ Sev │ Meta │');
187
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
188
+
189
+ for (let i = 0; i < deps.length; i++) {
190
+ const item = deps[i];
191
+ const num = String(i + 1).padStart(2, ' ');
192
+ const title = pad(truncate(item.title, 40), 40);
193
+ const bump = pad(truncate(item._deps?.bumpType || '', 7), 7);
194
+ const sev = pad(truncate(item.severity || 'info', 3), 3);
195
+ const meta = pad(truncate(item.meta || '', 4), 4);
196
+ lines.push(`│ ${num} │ ${title} │ ${bump} │ ${sev} │ ${meta} │`);
197
+ }
198
+
199
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
200
+ }
201
+
156
202
  // Render errors section
157
203
  if (errorResults.length > 0) {
158
204
  lines.push('');
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared utility functions for observe handlers
3
+ * Canonical source — all observe handlers import from here (OBS-10)
4
+ */
5
+
6
+ /**
7
+ * Parse a duration string like "7d", "24h", "30m" into milliseconds
8
+ * @param {string} duration - Duration string
9
+ * @returns {number} Milliseconds (0 for invalid input)
10
+ */
11
+ function parseDuration(duration) {
12
+ if (!duration) return 0;
13
+ const match = String(duration).match(/^(\d+)([dhms])$/);
14
+ if (!match) return 0;
15
+ const num = parseInt(match[1], 10);
16
+ const multipliers = { d: 86400000, h: 3600000, m: 60000, s: 1000 };
17
+ return num * (multipliers[match[2]] || 0);
18
+ }
19
+
20
+ /**
21
+ * Format age from ISO date to human-readable string
22
+ * @param {string} isoDate - ISO8601 date string
23
+ * @returns {string} Human-readable age like "5m", "2h", "3d"
24
+ */
25
+ function formatAge(isoDate) {
26
+ if (!isoDate) return 'unknown';
27
+ const diffMs = Date.now() - new Date(isoDate).getTime();
28
+ if (diffMs < 0) return 'future';
29
+ const minutes = Math.floor(diffMs / 60000);
30
+ if (minutes < 60) return `${minutes}m`;
31
+ const hours = Math.floor(minutes / 60);
32
+ if (hours < 24) return `${hours}h`;
33
+ const days = Math.floor(hours / 24);
34
+ return `${days}d`;
35
+ }
36
+
37
+ module.exports = { parseDuration, formatAge };