@nforma.ai/nforma 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,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 };