@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.
- package/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +40 -40
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/nf-precompact.test.js +227 -0
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-prompt.test.js +698 -0
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/nf-session-start.test.js +354 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/nf-slot-correlator.test.js +85 -0
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/nf-spec-regen.test.js +73 -0
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/nf-statusline.test.js +157 -0
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/nf-stop.test.js +1388 -0
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/nf-token-collector.test.js +262 -0
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +4 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- 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
|
+
};
|
package/bin/observe-handlers.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Source handler implementations for /
|
|
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,
|
package/bin/observe-render.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dual-table renderer for /
|
|
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 =>
|
|
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('
|
|
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(`
|
|
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 };
|