@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.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe config loader
|
|
3
|
+
* Loads source configuration from .planning/observe-sources.md (or triage-sources.md fallback)
|
|
4
|
+
* Parses YAML frontmatter, infers issue_type, applies defaults, validates required fields
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
// Source types that default to "issue"
|
|
11
|
+
const ISSUE_TYPES = ['github', 'sentry', 'sentry-feedback', 'bash'];
|
|
12
|
+
// Source types that default to "drift"
|
|
13
|
+
const DRIFT_TYPES = ['prometheus', 'grafana', 'logstash'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a YAML value string into appropriate JS type
|
|
17
|
+
* @param {string} val - Value string
|
|
18
|
+
* @returns {*} Parsed value
|
|
19
|
+
*/
|
|
20
|
+
function parseYamlValue(val) {
|
|
21
|
+
if (val === undefined || val === null || val === '') return '';
|
|
22
|
+
|
|
23
|
+
// Remove inline comments (but not inside quotes)
|
|
24
|
+
if (!val.startsWith('"') && !val.startsWith("'")) {
|
|
25
|
+
val = val.replace(/\s+#.*$/, '').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Boolean
|
|
29
|
+
if (val === 'true') return true;
|
|
30
|
+
if (val === 'false') return false;
|
|
31
|
+
|
|
32
|
+
// Null
|
|
33
|
+
if (val === 'null' || val === '~') return null;
|
|
34
|
+
|
|
35
|
+
// Number
|
|
36
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) return Number(val);
|
|
37
|
+
|
|
38
|
+
// Inline array [a, b, c]
|
|
39
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
40
|
+
const inner = val.slice(1, -1).trim();
|
|
41
|
+
if (!inner) return [];
|
|
42
|
+
return inner.split(',').map(s => parseYamlValue(s.trim()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Quoted string
|
|
46
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
47
|
+
return val.slice(1, -1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Minimal YAML parser for observe config frontmatter
|
|
55
|
+
* Handles: key-value pairs, nested objects, arrays of objects (- key: val), inline arrays
|
|
56
|
+
*
|
|
57
|
+
* @param {string} yamlStr - YAML string to parse
|
|
58
|
+
* @returns {object} Parsed object
|
|
59
|
+
*/
|
|
60
|
+
function parseSimpleYaml(yamlStr) {
|
|
61
|
+
const lines = yamlStr.split('\n');
|
|
62
|
+
|
|
63
|
+
// Pre-process: strip comments and track indent + content
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
// Preserve empty lines for structure but skip pure comment lines
|
|
67
|
+
const stripped = line.replace(/#.*$/, '');
|
|
68
|
+
const trimmed = stripped.trim();
|
|
69
|
+
if (!trimmed) continue;
|
|
70
|
+
const indent = stripped.search(/\S/);
|
|
71
|
+
entries.push({ indent, raw: trimmed });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parseBlock(entries, 0, -1).result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a block of YAML entries starting at index `start` with parent indent `parentIndent`.
|
|
79
|
+
* Returns { result, nextIndex }
|
|
80
|
+
*/
|
|
81
|
+
function parseBlock(entries, start, parentIndent) {
|
|
82
|
+
const result = {};
|
|
83
|
+
let i = start;
|
|
84
|
+
|
|
85
|
+
while (i < entries.length) {
|
|
86
|
+
const entry = entries[i];
|
|
87
|
+
|
|
88
|
+
// If this line is at or before parent indent, we're done with this block
|
|
89
|
+
if (entry.indent <= parentIndent) break;
|
|
90
|
+
|
|
91
|
+
const raw = entry.raw;
|
|
92
|
+
|
|
93
|
+
// Array item: "- key: val" or "- val"
|
|
94
|
+
if (raw.startsWith('- ')) {
|
|
95
|
+
// This shouldn't happen at top level without a key — skip
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Key: value
|
|
101
|
+
const colonIdx = raw.indexOf(':');
|
|
102
|
+
if (colonIdx === -1) {
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = raw.slice(0, colonIdx).trim();
|
|
108
|
+
const valStr = raw.slice(colonIdx + 1).trim();
|
|
109
|
+
|
|
110
|
+
if (valStr === '' || valStr === undefined) {
|
|
111
|
+
// Check what follows: nested object, array of objects, or empty
|
|
112
|
+
const nextIdx = i + 1;
|
|
113
|
+
if (nextIdx < entries.length && entries[nextIdx].indent > entry.indent) {
|
|
114
|
+
// Check if it's an array (next line starts with "- ")
|
|
115
|
+
if (entries[nextIdx].raw.startsWith('- ')) {
|
|
116
|
+
const arrResult = parseArray(entries, nextIdx, entry.indent);
|
|
117
|
+
result[key] = arrResult.items;
|
|
118
|
+
i = arrResult.nextIndex;
|
|
119
|
+
} else {
|
|
120
|
+
// Nested object
|
|
121
|
+
const blockResult = parseBlock(entries, nextIdx, entry.indent);
|
|
122
|
+
result[key] = blockResult.result;
|
|
123
|
+
i = blockResult.nextIndex;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
result[key] = {};
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
result[key] = parseYamlValue(valStr);
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { result, nextIndex: i };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse an array block starting at index `start`.
|
|
140
|
+
* Each "- key: val" starts a new object; subsequent indented lines add to it.
|
|
141
|
+
*/
|
|
142
|
+
function parseArray(entries, start, parentIndent) {
|
|
143
|
+
const items = [];
|
|
144
|
+
let i = start;
|
|
145
|
+
|
|
146
|
+
while (i < entries.length) {
|
|
147
|
+
const entry = entries[i];
|
|
148
|
+
|
|
149
|
+
// If we've dedented back to parent level or less, we're done
|
|
150
|
+
if (entry.indent <= parentIndent) break;
|
|
151
|
+
|
|
152
|
+
if (entry.raw.startsWith('- ')) {
|
|
153
|
+
// Start of a new array item
|
|
154
|
+
const content = entry.raw.slice(2).trim();
|
|
155
|
+
const arrayItemIndent = entry.indent;
|
|
156
|
+
|
|
157
|
+
if (content.includes(':')) {
|
|
158
|
+
// Object item
|
|
159
|
+
const item = {};
|
|
160
|
+
const colonIdx = content.indexOf(':');
|
|
161
|
+
const k = content.slice(0, colonIdx).trim();
|
|
162
|
+
const v = content.slice(colonIdx + 1).trim();
|
|
163
|
+
|
|
164
|
+
if (v === '') {
|
|
165
|
+
// Nested value under this array item key
|
|
166
|
+
const nextIdx = i + 1;
|
|
167
|
+
if (nextIdx < entries.length && entries[nextIdx].indent > arrayItemIndent) {
|
|
168
|
+
if (entries[nextIdx].raw.startsWith('- ')) {
|
|
169
|
+
const nestedArr = parseArray(entries, nextIdx, arrayItemIndent);
|
|
170
|
+
item[k] = nestedArr.items;
|
|
171
|
+
i = nestedArr.nextIndex;
|
|
172
|
+
} else {
|
|
173
|
+
const nestedBlock = parseBlock(entries, nextIdx, arrayItemIndent);
|
|
174
|
+
item[k] = nestedBlock.result;
|
|
175
|
+
i = nestedBlock.nextIndex;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
item[k] = {};
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
item[k] = parseYamlValue(v);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Collect additional key:val pairs for this array item (indented deeper than "- ")
|
|
187
|
+
while (i < entries.length && entries[i].indent > arrayItemIndent && !entries[i].raw.startsWith('- ')) {
|
|
188
|
+
const subEntry = entries[i];
|
|
189
|
+
const subColonIdx = subEntry.raw.indexOf(':');
|
|
190
|
+
if (subColonIdx !== -1) {
|
|
191
|
+
const sk = subEntry.raw.slice(0, subColonIdx).trim();
|
|
192
|
+
const sv = subEntry.raw.slice(subColonIdx + 1).trim();
|
|
193
|
+
|
|
194
|
+
if (sv === '') {
|
|
195
|
+
// Nested block under this key
|
|
196
|
+
const nextIdx = i + 1;
|
|
197
|
+
if (nextIdx < entries.length && entries[nextIdx].indent > subEntry.indent) {
|
|
198
|
+
if (entries[nextIdx].raw.startsWith('- ')) {
|
|
199
|
+
const nestedArr = parseArray(entries, nextIdx, subEntry.indent);
|
|
200
|
+
item[sk] = nestedArr.items;
|
|
201
|
+
i = nestedArr.nextIndex;
|
|
202
|
+
} else {
|
|
203
|
+
const nestedBlock = parseBlock(entries, nextIdx, subEntry.indent);
|
|
204
|
+
item[sk] = nestedBlock.result;
|
|
205
|
+
i = nestedBlock.nextIndex;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
item[sk] = {};
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
item[sk] = parseYamlValue(sv);
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
items.push(item);
|
|
221
|
+
} else {
|
|
222
|
+
// Simple value array item
|
|
223
|
+
items.push(parseYamlValue(content));
|
|
224
|
+
i++;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Not an array item, done with this array
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { items, nextIndex: i };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Load observe configuration from YAML frontmatter file
|
|
237
|
+
*
|
|
238
|
+
* @param {string} [configPath] - Optional explicit config file path
|
|
239
|
+
* @param {string} [basePath] - Base directory (default: process.cwd())
|
|
240
|
+
* @returns {object} { sources, configFile, observeConfig, error? }
|
|
241
|
+
*/
|
|
242
|
+
function loadObserveConfig(configPath, basePath) {
|
|
243
|
+
const base = basePath || process.cwd();
|
|
244
|
+
|
|
245
|
+
// Resolve config file: explicit path > observe-sources.md > triage-sources.md
|
|
246
|
+
let configFile = null;
|
|
247
|
+
if (configPath) {
|
|
248
|
+
const resolved = path.resolve(base, configPath);
|
|
249
|
+
if (fs.existsSync(resolved)) {
|
|
250
|
+
configFile = resolved;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!configFile) {
|
|
255
|
+
const observePath = path.resolve(base, '.planning/observe-sources.md');
|
|
256
|
+
if (fs.existsSync(observePath)) {
|
|
257
|
+
configFile = observePath;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!configFile) {
|
|
262
|
+
const triagePath = path.resolve(base, '.planning/triage-sources.md');
|
|
263
|
+
if (fs.existsSync(triagePath)) {
|
|
264
|
+
configFile = triagePath;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!configFile) {
|
|
269
|
+
return {
|
|
270
|
+
sources: [],
|
|
271
|
+
configFile: null,
|
|
272
|
+
observeConfig: {},
|
|
273
|
+
error: 'No observe sources configured'
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Read and parse frontmatter
|
|
278
|
+
const content = fs.readFileSync(configFile, 'utf8');
|
|
279
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
280
|
+
if (!fmMatch) {
|
|
281
|
+
return {
|
|
282
|
+
sources: [],
|
|
283
|
+
configFile,
|
|
284
|
+
observeConfig: {},
|
|
285
|
+
error: 'No YAML frontmatter found in config file'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const frontmatter = parseSimpleYaml(fmMatch[1]);
|
|
290
|
+
|
|
291
|
+
// Extract observe_config global settings
|
|
292
|
+
const observeConfig = frontmatter.observe_config || {};
|
|
293
|
+
const defaultTimeout = observeConfig.default_timeout ?? 10;
|
|
294
|
+
const failOpenDefault = observeConfig.fail_open_default ?? true;
|
|
295
|
+
|
|
296
|
+
// Extract sources array
|
|
297
|
+
let sources = [];
|
|
298
|
+
if (Array.isArray(frontmatter.sources)) {
|
|
299
|
+
sources = frontmatter.sources;
|
|
300
|
+
} else if (observeConfig && Array.isArray(observeConfig.sources)) {
|
|
301
|
+
sources = observeConfig.sources;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Validate and apply defaults
|
|
305
|
+
const validationErrors = [];
|
|
306
|
+
sources = sources.map((source, idx) => {
|
|
307
|
+
const errors = [];
|
|
308
|
+
|
|
309
|
+
// Required fields
|
|
310
|
+
if (!source.type || typeof source.type !== 'string') {
|
|
311
|
+
errors.push(`sources[${idx}]: type required (string)`);
|
|
312
|
+
}
|
|
313
|
+
if (!source.label || typeof source.label !== 'string') {
|
|
314
|
+
errors.push(`sources[${idx}]: label required (string)`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (errors.length > 0) {
|
|
318
|
+
validationErrors.push(...errors);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Infer issue_type if not specified
|
|
322
|
+
if (!source.issue_type) {
|
|
323
|
+
if (ISSUE_TYPES.includes(source.type)) {
|
|
324
|
+
source.issue_type = 'issue';
|
|
325
|
+
} else if (DRIFT_TYPES.includes(source.type)) {
|
|
326
|
+
source.issue_type = 'drift';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Apply defaults
|
|
331
|
+
source.timeout = source.timeout ?? defaultTimeout;
|
|
332
|
+
source.fail_open = source.fail_open ?? failOpenDefault;
|
|
333
|
+
|
|
334
|
+
return source;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = {
|
|
338
|
+
sources,
|
|
339
|
+
configFile,
|
|
340
|
+
observeConfig: {
|
|
341
|
+
default_timeout: defaultTimeout,
|
|
342
|
+
fail_open_default: failOpenDefault
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (validationErrors.length > 0) {
|
|
347
|
+
result.error = validationErrors.join('; ');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { loadObserveConfig, parseSimpleYaml, parseYamlValue };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debt ledger write-through for /qgsd:observe
|
|
3
|
+
* Upserts observations to .planning/formal/debt.json by fingerprint using v0.27-01 functions
|
|
4
|
+
* Then runs dedup engine and formal reference linker (v0.27-03)
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL: Never compute fingerprints inline — always use imported v0.27-01 functions
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { readDebtLedger, writeDebtLedger } = require('./debt-ledger.cjs');
|
|
12
|
+
const { fingerprintIssue } = require('./fingerprint-issue.cjs');
|
|
13
|
+
const { fingerprintDrift } = require('./fingerprint-drift.cjs');
|
|
14
|
+
const { validateDebtEntry } = require('./validate-debt-entry.cjs');
|
|
15
|
+
const { deduplicateEntries } = require('./debt-dedup.cjs');
|
|
16
|
+
const { linkFormalRefs } = require('./formal-ref-linker.cjs');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Write observations from observe command to debt ledger, then dedup and link formal refs.
|
|
20
|
+
* Pipeline: write/upsert -> dedup (fingerprint + Levenshtein) -> formal-ref link -> save
|
|
21
|
+
*
|
|
22
|
+
* @param {object[]} observations - Array of issue/drift objects from handlers (standard schema)
|
|
23
|
+
* @param {string} [ledgerPath] - Path to debt.json (default: .planning/formal/debt.json)
|
|
24
|
+
* @param {object} [options] - Options for dedup and linking
|
|
25
|
+
* @param {number} [options.threshold=0.85] - Levenshtein similarity threshold
|
|
26
|
+
* @param {boolean} [options.verbose=false] - Include detailed merge/link logs
|
|
27
|
+
* @param {string} [options.requirementsPath] - Custom requirements.json path
|
|
28
|
+
* @param {string} [options.specDir] - Custom spec directory path
|
|
29
|
+
* @returns {object} { written, updated, errors, merged, linked, mergeLog?, linkLog? }
|
|
30
|
+
*/
|
|
31
|
+
function writeObservationsToDebt(observations, ledgerPath, options = {}) {
|
|
32
|
+
const resolvedPath = ledgerPath || path.resolve(process.cwd(), '.planning/formal/debt.json');
|
|
33
|
+
const ledger = readDebtLedger(resolvedPath);
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
|
|
36
|
+
let written = 0;
|
|
37
|
+
let updated = 0;
|
|
38
|
+
let errors = 0;
|
|
39
|
+
|
|
40
|
+
for (const obs of observations) {
|
|
41
|
+
try {
|
|
42
|
+
// Compute fingerprint using v0.27-01 functions (NEVER inline)
|
|
43
|
+
let fp;
|
|
44
|
+
if (obs.issue_type === 'drift') {
|
|
45
|
+
fp = fingerprintDrift({
|
|
46
|
+
formal_parameter_key: obs.formal_parameter_key || obs.title
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
fp = fingerprintIssue({
|
|
50
|
+
exception_type: obs.exception_type || obs.source_type || 'unknown',
|
|
51
|
+
function_name: obs.function_name || 'unknown',
|
|
52
|
+
message: obs.title || ''
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Search for existing entry by fingerprint
|
|
57
|
+
const existingIdx = ledger.debt_entries.findIndex(e => e.fingerprint === fp);
|
|
58
|
+
|
|
59
|
+
if (existingIdx >= 0) {
|
|
60
|
+
// Update existing entry
|
|
61
|
+
const existing = ledger.debt_entries[existingIdx];
|
|
62
|
+
existing.occurrences = (existing.occurrences || 1) + 1;
|
|
63
|
+
existing.last_seen = now;
|
|
64
|
+
existing.source_entries.push({
|
|
65
|
+
source_type: obs.source_type || 'unknown',
|
|
66
|
+
source_id: obs.id || `obs-${Date.now()}`,
|
|
67
|
+
observed_at: obs.created_at || now
|
|
68
|
+
});
|
|
69
|
+
updated++;
|
|
70
|
+
} else {
|
|
71
|
+
// Create new debt entry
|
|
72
|
+
const entry = {
|
|
73
|
+
id: crypto.randomUUID(),
|
|
74
|
+
fingerprint: fp,
|
|
75
|
+
title: (obs.title || 'Unknown observation').slice(0, 256),
|
|
76
|
+
occurrences: 1,
|
|
77
|
+
first_seen: now,
|
|
78
|
+
last_seen: now,
|
|
79
|
+
environments: ['production'],
|
|
80
|
+
status: 'open',
|
|
81
|
+
formal_ref: obs.formal_parameter_key || null,
|
|
82
|
+
source_entries: [{
|
|
83
|
+
source_type: obs.source_type || 'unknown',
|
|
84
|
+
source_id: obs.id || `obs-${Date.now()}`,
|
|
85
|
+
observed_at: obs.created_at || now
|
|
86
|
+
}]
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Validate before adding
|
|
90
|
+
const validation = validateDebtEntry(entry);
|
|
91
|
+
if (validation !== true) {
|
|
92
|
+
console.warn(`[observe-debt-writer] Skipping invalid entry: ${validation.join('; ')}`);
|
|
93
|
+
errors++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ledger.debt_entries.push(entry);
|
|
98
|
+
written++;
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`[observe-debt-writer] Error processing observation: ${err.message}`);
|
|
102
|
+
errors++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Phase 2: Dedup (fingerprint exact-match + Levenshtein near-duplicate)
|
|
107
|
+
const dedupResult = deduplicateEntries(ledger.debt_entries, {
|
|
108
|
+
threshold: options.threshold ?? 0.85
|
|
109
|
+
});
|
|
110
|
+
ledger.debt_entries = dedupResult.entries;
|
|
111
|
+
|
|
112
|
+
// Phase 3: Formal reference linking
|
|
113
|
+
const linkResult = linkFormalRefs(ledger.debt_entries, {
|
|
114
|
+
requirementsPath: options.requirementsPath,
|
|
115
|
+
specDir: options.specDir
|
|
116
|
+
});
|
|
117
|
+
ledger.debt_entries = linkResult.entries;
|
|
118
|
+
|
|
119
|
+
// Write updated ledger only if changes occurred
|
|
120
|
+
if (written > 0 || updated > 0 || dedupResult.mergeCount > 0 || linkResult.linkedCount > 0) {
|
|
121
|
+
writeDebtLedger(resolvedPath, ledger);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = {
|
|
125
|
+
written,
|
|
126
|
+
updated,
|
|
127
|
+
errors,
|
|
128
|
+
merged: dedupResult.mergeCount,
|
|
129
|
+
linked: linkResult.linkedCount
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (options.verbose) {
|
|
133
|
+
result.mergeLog = dedupResult.mergeLog;
|
|
134
|
+
result.linkLog = linkResult.linkLog;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { writeObservationsToDebt };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grafana source handler for /qgsd:observe
|
|
3
|
+
* Fetches alert rules from Grafana unified alerting API
|
|
4
|
+
* Returns standard issue schema for the observe registry
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format age from ISO date to human-readable string
|
|
9
|
+
* @param {string} isoDate - ISO8601 date string
|
|
10
|
+
* @returns {string} Human-readable age
|
|
11
|
+
*/
|
|
12
|
+
function formatAge(isoDate) {
|
|
13
|
+
if (!isoDate) return 'unknown';
|
|
14
|
+
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
15
|
+
if (diffMs < 0) return 'future';
|
|
16
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
17
|
+
if (minutes < 60) return `${minutes}m`;
|
|
18
|
+
const hours = Math.floor(minutes / 60);
|
|
19
|
+
if (hours < 24) return `${hours}h`;
|
|
20
|
+
const days = Math.floor(hours / 24);
|
|
21
|
+
return `${days}d`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map Grafana alert state to severity
|
|
26
|
+
* @param {string} state - Grafana alert state
|
|
27
|
+
* @returns {string} Severity level
|
|
28
|
+
*/
|
|
29
|
+
function mapStateSeverity(state) {
|
|
30
|
+
const mapping = {
|
|
31
|
+
alerting: 'error',
|
|
32
|
+
firing: 'error',
|
|
33
|
+
pending: 'warning',
|
|
34
|
+
nodata: 'warning',
|
|
35
|
+
normal: 'info',
|
|
36
|
+
ok: 'info',
|
|
37
|
+
paused: 'info'
|
|
38
|
+
};
|
|
39
|
+
return mapping[state] || 'info';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Grafana source handler
|
|
44
|
+
* Fetches alert rules from Grafana unified alerting API and maps to standard schema
|
|
45
|
+
*
|
|
46
|
+
* @param {object} sourceConfig - { type, label, endpoint, auth_env?, issue_type? }
|
|
47
|
+
* @param {object} options - { fetchFn? }
|
|
48
|
+
* @returns {Promise<object>} Standard schema result
|
|
49
|
+
*/
|
|
50
|
+
async function handleGrafana(sourceConfig, options) {
|
|
51
|
+
const label = sourceConfig.label || 'Grafana';
|
|
52
|
+
const endpoint = (sourceConfig.endpoint || '').replace(/\/$/, '');
|
|
53
|
+
const fetchFn = (options && options.fetchFn) || globalThis.fetch;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Build auth headers
|
|
57
|
+
const headers = {};
|
|
58
|
+
if (sourceConfig.auth_env) {
|
|
59
|
+
const token = process.env[sourceConfig.auth_env];
|
|
60
|
+
if (token) {
|
|
61
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const url = `${endpoint}/api/v1/provisioning/alert-rules`;
|
|
66
|
+
const response = await fetchFn(url, { headers });
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
return {
|
|
70
|
+
source_label: label,
|
|
71
|
+
source_type: 'grafana',
|
|
72
|
+
status: 'error',
|
|
73
|
+
error: `HTTP ${response.status} from Grafana`,
|
|
74
|
+
issues: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rules = await response.json();
|
|
79
|
+
const ruleList = Array.isArray(rules) ? rules : [];
|
|
80
|
+
|
|
81
|
+
const issues = ruleList.map((rule, idx) => {
|
|
82
|
+
const labels = rule.labels || {};
|
|
83
|
+
const annotations = rule.annotations || {};
|
|
84
|
+
const state = labels.grafana_state || '';
|
|
85
|
+
const severity = mapStateSeverity(state);
|
|
86
|
+
|
|
87
|
+
// Build URL from dashboardUid if available
|
|
88
|
+
const ruleUrl = rule.dashboardUid
|
|
89
|
+
? `${endpoint}/d/${rule.dashboardUid}`
|
|
90
|
+
: endpoint;
|
|
91
|
+
|
|
92
|
+
// Build meta from annotations and context
|
|
93
|
+
const metaParts = [];
|
|
94
|
+
if (annotations.summary) metaParts.push(annotations.summary);
|
|
95
|
+
if (rule.ruleGroup) metaParts.push(`group: ${rule.ruleGroup}`);
|
|
96
|
+
if (rule.folderUID) metaParts.push(`folder: ${rule.folderUID}`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id: `grafana-alert-${rule.id || idx}`,
|
|
100
|
+
title: rule.title || `alert-rule-${idx}`,
|
|
101
|
+
severity,
|
|
102
|
+
url: ruleUrl,
|
|
103
|
+
age: formatAge(rule.updated),
|
|
104
|
+
created_at: rule.updated || new Date().toISOString(),
|
|
105
|
+
meta: metaParts.join(' | '),
|
|
106
|
+
source_type: 'grafana',
|
|
107
|
+
issue_type: sourceConfig.issue_type || 'drift'
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
source_label: label,
|
|
113
|
+
source_type: 'grafana',
|
|
114
|
+
status: 'ok',
|
|
115
|
+
issues
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
source_label: label,
|
|
120
|
+
source_type: 'grafana',
|
|
121
|
+
status: 'error',
|
|
122
|
+
error: `Grafana fetch failed: ${err.message}`,
|
|
123
|
+
issues: []
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { handleGrafana };
|