@maintainabilityai/research-runner 0.1.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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/bin/research-runner.js +2 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +209 -0
  6. package/dist/llm/anthropic-client.d.ts +39 -0
  7. package/dist/llm/anthropic-client.js +74 -0
  8. package/dist/llm/github-models-client.d.ts +46 -0
  9. package/dist/llm/github-models-client.js +78 -0
  10. package/dist/llm/llm-router.d.ts +46 -0
  11. package/dist/llm/llm-router.js +60 -0
  12. package/dist/mesh/get-mesh-sha.d.ts +1 -0
  13. package/dist/mesh/get-mesh-sha.js +27 -0
  14. package/dist/mesh/mesh-reader.d.ts +14 -0
  15. package/dist/mesh/mesh-reader.js +392 -0
  16. package/dist/mesh/prompt-loader.d.ts +22 -0
  17. package/dist/mesh/prompt-loader.js +119 -0
  18. package/dist/mesh/threat-model-reader.d.ts +33 -0
  19. package/dist/mesh/threat-model-reader.js +123 -0
  20. package/dist/runner/archeologist.d.ts +39 -0
  21. package/dist/runner/archeologist.js +620 -0
  22. package/dist/runner/audit-emitter.d.ts +62 -0
  23. package/dist/runner/audit-emitter.js +210 -0
  24. package/dist/runner/hatters-tag-builder.d.ts +52 -0
  25. package/dist/runner/hatters-tag-builder.js +40 -0
  26. package/dist/runner/nodes/analyze-architecture.d.ts +10 -0
  27. package/dist/runner/nodes/analyze-architecture.js +447 -0
  28. package/dist/runner/nodes/arxiv-search.d.ts +12 -0
  29. package/dist/runner/nodes/arxiv-search.js +52 -0
  30. package/dist/runner/nodes/clone-and-index.d.ts +32 -0
  31. package/dist/runner/nodes/clone-and-index.js +158 -0
  32. package/dist/runner/nodes/dedupe-and-rank.d.ts +27 -0
  33. package/dist/runner/nodes/dedupe-and-rank.js +98 -0
  34. package/dist/runner/nodes/deterministic-review.d.ts +55 -0
  35. package/dist/runner/nodes/deterministic-review.js +206 -0
  36. package/dist/runner/nodes/expert-review.d.ts +68 -0
  37. package/dist/runner/nodes/expert-review.js +197 -0
  38. package/dist/runner/nodes/gap-analysis.d.ts +48 -0
  39. package/dist/runner/nodes/gap-analysis.js +153 -0
  40. package/dist/runner/nodes/generate-prd-manifest.d.ts +53 -0
  41. package/dist/runner/nodes/generate-prd-manifest.js +209 -0
  42. package/dist/runner/nodes/hackernews-search.d.ts +12 -0
  43. package/dist/runner/nodes/hackernews-search.js +63 -0
  44. package/dist/runner/nodes/identify-gaps.d.ts +33 -0
  45. package/dist/runner/nodes/identify-gaps.js +185 -0
  46. package/dist/runner/nodes/plan-queries.d.ts +28 -0
  47. package/dist/runner/nodes/plan-queries.js +120 -0
  48. package/dist/runner/nodes/prd-validator.d.ts +51 -0
  49. package/dist/runner/nodes/prd-validator.js +203 -0
  50. package/dist/runner/nodes/synthesis-archaeology-validator.d.ts +22 -0
  51. package/dist/runner/nodes/synthesis-archaeology-validator.js +131 -0
  52. package/dist/runner/nodes/synthesis-validator.d.ts +51 -0
  53. package/dist/runner/nodes/synthesis-validator.js +185 -0
  54. package/dist/runner/nodes/synthesize-prd.d.ts +84 -0
  55. package/dist/runner/nodes/synthesize-prd.js +202 -0
  56. package/dist/runner/nodes/synthesize-report.d.ts +53 -0
  57. package/dist/runner/nodes/synthesize-report.js +188 -0
  58. package/dist/runner/nodes/tavily-search.d.ts +21 -0
  59. package/dist/runner/nodes/tavily-search.js +57 -0
  60. package/dist/runner/nodes/uspto-search.d.ts +13 -0
  61. package/dist/runner/nodes/uspto-search.js +62 -0
  62. package/dist/runner/nodes/verify-grounding.d.ts +54 -0
  63. package/dist/runner/nodes/verify-grounding.js +134 -0
  64. package/dist/runner/prd.d.ts +28 -0
  65. package/dist/runner/prd.js +494 -0
  66. package/dist/schemas/audit-event.d.ts +1151 -0
  67. package/dist/schemas/audit-event.js +141 -0
  68. package/dist/schemas/index.d.ts +17 -0
  69. package/dist/schemas/index.js +33 -0
  70. package/dist/schemas/mesh-context.d.ts +415 -0
  71. package/dist/schemas/mesh-context.js +95 -0
  72. package/dist/schemas/observed-architecture.d.ts +262 -0
  73. package/dist/schemas/observed-architecture.js +90 -0
  74. package/dist/schemas/prd-brief.d.ts +111 -0
  75. package/dist/schemas/prd-brief.js +37 -0
  76. package/dist/schemas/prd-doc.d.ts +249 -0
  77. package/dist/schemas/prd-doc.js +42 -0
  78. package/dist/schemas/prd-manifest.d.ts +171 -0
  79. package/dist/schemas/prd-manifest.js +73 -0
  80. package/dist/schemas/primitives.d.ts +47 -0
  81. package/dist/schemas/primitives.js +41 -0
  82. package/dist/schemas/query-plan.d.ts +33 -0
  83. package/dist/schemas/query-plan.js +25 -0
  84. package/dist/schemas/ranked-source.d.ts +82 -0
  85. package/dist/schemas/ranked-source.js +29 -0
  86. package/dist/schemas/research-brief.d.ts +114 -0
  87. package/dist/schemas/research-brief.js +49 -0
  88. package/dist/schemas/research-doc.d.ts +104 -0
  89. package/dist/schemas/research-doc.js +37 -0
  90. package/dist/search/arxiv-client.d.ts +41 -0
  91. package/dist/search/arxiv-client.js +88 -0
  92. package/dist/search/hackernews-client.d.ts +33 -0
  93. package/dist/search/hackernews-client.js +44 -0
  94. package/dist/search/provider-result.d.ts +25 -0
  95. package/dist/search/provider-result.js +2 -0
  96. package/dist/search/tavily-client.d.ts +38 -0
  97. package/dist/search/tavily-client.js +53 -0
  98. package/dist/search/uspto-client.d.ts +50 -0
  99. package/dist/search/uspto-client.js +112 -0
  100. package/dist/utils/run-id.d.ts +2 -0
  101. package/dist/utils/run-id.js +22 -0
  102. package/package.json +53 -0
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMeshSha = getMeshSha;
4
+ /**
5
+ * get-mesh-sha — pure helper for resolving the mesh repo's git HEAD SHA.
6
+ *
7
+ * Mirrors the canonical impl in vscode-extension/src/core/mesh-sha.ts. We
8
+ * duplicate rather than depend on the extension because the runner ships as
9
+ * a separate npm package and pulling in the extension would be a layering
10
+ * violation.
11
+ */
12
+ const node_child_process_1 = require("node:child_process");
13
+ const SHA_RE = /^[0-9a-f]{7,40}$/;
14
+ function getMeshSha(meshPath) {
15
+ try {
16
+ const out = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], {
17
+ cwd: meshPath,
18
+ encoding: 'utf8',
19
+ stdio: ['ignore', 'pipe', 'ignore'],
20
+ });
21
+ const sha = out.trim();
22
+ return SHA_RE.test(sha) ? sha : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { MeshContext } from '../schemas';
2
+ export interface GatherMeshContextOpts {
3
+ meshDir: string;
4
+ /** Scope: 'platform' or 'bar'. Portfolio scope was removed — runs
5
+ * must anchor to a specific platform or BAR for the agents to
6
+ * produce a targeted PRD. */
7
+ scope: {
8
+ level: 'platform' | 'bar';
9
+ id: string;
10
+ };
11
+ }
12
+ /** Normalize a GitHub URL (or already-slug form) to `owner/repo`. */
13
+ export declare function normalizeRepoSlug(value: string): string | null;
14
+ export declare function gatherMeshContext(opts: GatherMeshContextOpts): MeshContext;
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.normalizeRepoSlug = normalizeRepoSlug;
37
+ exports.gatherMeshContext = gatherMeshContext;
38
+ /**
39
+ * mesh-reader — walks a governance mesh on disk and produces a MeshContext
40
+ * for the Archeologist / PRD agents.
41
+ *
42
+ * Scope resolution:
43
+ * - portfolio: reads mesh.yaml only
44
+ * - platform: reads mesh.yaml + platforms/<slug>/platform.yaml + platform.arch.json
45
+ * - bar: reads everything above + the BAR's app.yaml + bar.arch.json
46
+ * + threat-model.yaml + ADRs/ + research/ + prds/
47
+ *
48
+ * BAR id → path resolution walks platforms/*\/bars/* looking for an `app.yaml`
49
+ * whose `id:` field matches. Cheap on small portfolios, fine for v1.
50
+ */
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ const yaml = __importStar(require("js-yaml"));
54
+ const get_mesh_sha_1 = require("./get-mesh-sha");
55
+ const threat_model_reader_1 = require("./threat-model-reader");
56
+ const STALE_RESEARCH_DAYS = 90;
57
+ /** Normalize a GitHub URL (or already-slug form) to `owner/repo`. */
58
+ function normalizeRepoSlug(value) {
59
+ if (!value) {
60
+ return null;
61
+ }
62
+ const trimmed = value.trim().replace(/\.git$/, '').replace(/\/$/, '');
63
+ // Already in owner/repo form
64
+ if (/^[\w.-]+\/[\w.-]+$/.test(trimmed)) {
65
+ return trimmed;
66
+ }
67
+ // https://github.com/owner/repo or git@github.com:owner/repo
68
+ const httpsMatch = trimmed.match(/github\.com[:/]([\w.-]+\/[\w.-]+)$/i);
69
+ if (httpsMatch) {
70
+ return httpsMatch[1];
71
+ }
72
+ return null;
73
+ }
74
+ /** Pull repo URLs from app.yaml and normalize each to `owner/repo`; drop unparseable entries. */
75
+ function extractLinkedRepos(appYaml) {
76
+ const raw = appYaml?.application?.repos;
77
+ if (!Array.isArray(raw)) {
78
+ return [];
79
+ }
80
+ const out = [];
81
+ for (const entry of raw) {
82
+ if (typeof entry !== 'string') {
83
+ continue;
84
+ }
85
+ const slug = normalizeRepoSlug(entry);
86
+ if (slug) {
87
+ out.push(slug);
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ function loadYamlFile(p) {
93
+ try {
94
+ return yaml.load(fs.readFileSync(p, 'utf8'));
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ function loadJsonFile(p) {
101
+ try {
102
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /** Scan a `<bar>/<subdir>/` for .md docs; return newest-first metadata. */
109
+ function scanDocDir(barPath, subdir) {
110
+ const dir = path.join(barPath, subdir);
111
+ if (!fs.existsSync(dir)) {
112
+ return [];
113
+ }
114
+ let entries;
115
+ try {
116
+ entries = fs.readdirSync(dir, { withFileTypes: true });
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ const summaries = [];
122
+ for (const entry of entries) {
123
+ if (!entry.isFile() || !entry.name.endsWith('.md')) {
124
+ continue;
125
+ }
126
+ const filePath = path.join(dir, entry.name);
127
+ const id = entry.name.replace(/\.md$/, '');
128
+ let mtimeMs = 0;
129
+ let publishedAt = new Date(0).toISOString();
130
+ let topic = id.replace(/[-_]/g, ' ');
131
+ try {
132
+ const stat = fs.statSync(filePath);
133
+ mtimeMs = stat.mtimeMs;
134
+ publishedAt = stat.mtime.toISOString();
135
+ const content = fs.readFileSync(filePath, 'utf8');
136
+ const headingMatch = content.match(/^#\s+(.+)$/m);
137
+ if (headingMatch) {
138
+ topic = headingMatch[1].trim();
139
+ }
140
+ }
141
+ catch { /* fall through with defaults */ }
142
+ summaries.push({ research_id: id, topic, published_at: publishedAt, _mtime_ms: mtimeMs });
143
+ }
144
+ return summaries.sort((a, b) => b._mtime_ms - a._mtime_ms);
145
+ }
146
+ function stripMtime(s) {
147
+ const { _mtime_ms: _unused, ...rest } = s;
148
+ void _unused;
149
+ return rest;
150
+ }
151
+ /** Locate a BAR by id by walking `platforms/<slug>/bars/<id>/app.yaml`. */
152
+ function findBarPath(meshDir, barId) {
153
+ const platformsDir = path.join(meshDir, 'platforms');
154
+ if (!fs.existsSync(platformsDir)) {
155
+ return null;
156
+ }
157
+ for (const platformEntry of fs.readdirSync(platformsDir, { withFileTypes: true })) {
158
+ if (!platformEntry.isDirectory()) {
159
+ continue;
160
+ }
161
+ const barsDir = path.join(platformsDir, platformEntry.name, 'bars');
162
+ if (!fs.existsSync(barsDir)) {
163
+ continue;
164
+ }
165
+ for (const barEntry of fs.readdirSync(barsDir, { withFileTypes: true })) {
166
+ if (!barEntry.isDirectory()) {
167
+ continue;
168
+ }
169
+ const candidate = path.join(barsDir, barEntry.name);
170
+ const appYaml = loadYamlFile(path.join(candidate, 'app.yaml'));
171
+ if (appYaml?.application?.id === barId) {
172
+ return { barPath: candidate, platformSlug: platformEntry.name };
173
+ }
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ function listSiblingBars(platformDir, excludeBarId) {
179
+ const barsDir = path.join(platformDir, 'bars');
180
+ if (!fs.existsSync(barsDir)) {
181
+ return [];
182
+ }
183
+ const out = [];
184
+ for (const entry of fs.readdirSync(barsDir, { withFileTypes: true })) {
185
+ if (!entry.isDirectory()) {
186
+ continue;
187
+ }
188
+ const barPath = path.join(barsDir, entry.name);
189
+ const appYaml = loadYamlFile(path.join(barPath, 'app.yaml'));
190
+ const app = appYaml?.application;
191
+ if (!app?.id || app.id === excludeBarId) {
192
+ continue;
193
+ }
194
+ // CALM nodes the BAR owns — same shape as the bar-scope branch.
195
+ const calm = loadJsonFile(path.join(barPath, 'architecture', 'bar.arch.json'));
196
+ const calmNodeIds = extractCalmNodeIdsFromArchJson(calm);
197
+ // Threat ids — read via the same threat-model-reader used at bar scope.
198
+ const tm = (0, threat_model_reader_1.readThreatModelFromBar)(barPath);
199
+ const threatIds = Array.isArray(tm?.threats)
200
+ ? tm.threats.map(t => t.id).filter((id) => !!id)
201
+ : [];
202
+ out.push({
203
+ bar_id: app.id,
204
+ name: app.name || app.id,
205
+ composite_score: 0, // v1: scorer integration lands in phase 4
206
+ linked_repos: extractLinkedRepos(appYaml),
207
+ calm_node_ids: calmNodeIds,
208
+ threat_ids: threatIds,
209
+ });
210
+ }
211
+ return out;
212
+ }
213
+ /** Pull `unique-id` strings out of the nodes array of a parsed bar.arch.json. */
214
+ function extractCalmNodeIdsFromArchJson(calm) {
215
+ if (!calm || typeof calm !== 'object') {
216
+ return [];
217
+ }
218
+ const nodes = calm.nodes;
219
+ if (!Array.isArray(nodes)) {
220
+ return [];
221
+ }
222
+ const out = [];
223
+ for (const n of nodes) {
224
+ const id = n['unique-id'];
225
+ if (typeof id === 'string' && id.length > 0) {
226
+ out.push(id);
227
+ }
228
+ }
229
+ return out;
230
+ }
231
+ function readAdrs(barPath) {
232
+ const adrDir = path.join(barPath, 'architecture', 'ADRs');
233
+ if (!fs.existsSync(adrDir)) {
234
+ return [];
235
+ }
236
+ const out = [];
237
+ for (const entry of fs.readdirSync(adrDir, { withFileTypes: true })) {
238
+ if (!entry.isFile() || !entry.name.endsWith('.md')) {
239
+ continue;
240
+ }
241
+ const content = (() => { try {
242
+ return fs.readFileSync(path.join(adrDir, entry.name), 'utf8');
243
+ }
244
+ catch {
245
+ return '';
246
+ } })();
247
+ if (!content) {
248
+ continue;
249
+ }
250
+ const idMatch = content.match(/^#\s*ADR[-\s]?(\d+)/im);
251
+ const titleMatch = content.match(/^#\s+(.+)/m);
252
+ const statusMatch = content.match(/^##\s+Status\s*\n+\s*(\S+)/im);
253
+ const decisionMatch = content.match(/^##\s+Decision\s*\n+([\s\S]*?)(?=\n##\s|\n$)/im);
254
+ out.push({
255
+ id: idMatch ? `ADR-${idMatch[1].padStart(4, '0')}` : entry.name.replace(/\.md$/, ''),
256
+ title: (titleMatch?.[1] || entry.name).trim(),
257
+ status: (statusMatch?.[1] || 'unknown').trim(),
258
+ decision: (decisionMatch?.[1] || '').trim().slice(0, 500),
259
+ });
260
+ }
261
+ return out.sort((a, b) => a.id.localeCompare(b.id));
262
+ }
263
+ /**
264
+ * Pillar-score loading is a phase-4 integration point. For phase 2 we return
265
+ * a zeroed placeholder so the schema validates; later we'll either re-export
266
+ * GovernanceScorer into a shared package or shell out to a `scorecard` CLI.
267
+ */
268
+ function placeholderPillarScores() {
269
+ return { architecture: 0, security: 0, info_risk: 0, operations: 0 };
270
+ }
271
+ /**
272
+ * Detect mesh gaps from disk alone — does NOT require pillar scores. Mirrors
273
+ * the same gap-kind enum as GovernanceScorer.detectMeshGaps in vscode-extension.
274
+ * Returns the subset that can be detected without the scorer.
275
+ */
276
+ function detectMeshGapsFromDisk(barPath, research, adrCount) {
277
+ const gaps = [];
278
+ if (!(0, threat_model_reader_1.readThreatModelFromBar)(barPath)) {
279
+ gaps.push('no_threat_model');
280
+ }
281
+ try {
282
+ const controlsStat = fs.statSync(path.join(barPath, 'security', 'security-controls.yaml'));
283
+ if (controlsStat.size === 0) {
284
+ gaps.push('no_controls_mapping');
285
+ }
286
+ }
287
+ catch {
288
+ gaps.push('no_controls_mapping');
289
+ }
290
+ if (adrCount === 0) {
291
+ gaps.push('no_adrs');
292
+ }
293
+ if (research.length > 0) {
294
+ const cutoff = Date.now() - STALE_RESEARCH_DAYS * 24 * 60 * 60 * 1000;
295
+ const allStale = research.every(r => r._mtime_ms < cutoff);
296
+ if (allStale) {
297
+ gaps.push('stale_research');
298
+ }
299
+ }
300
+ return gaps;
301
+ }
302
+ function gatherMeshContext(opts) {
303
+ const meshDir = path.resolve(opts.meshDir);
304
+ const meshSha = (0, get_mesh_sha_1.getMeshSha)(meshDir);
305
+ if (!meshSha) {
306
+ throw new Error(`Could not resolve mesh git SHA at ${meshDir}. Is it a git repo with at least one commit?`);
307
+ }
308
+ const portfolio = loadYamlFile(path.join(meshDir, 'mesh.yaml')) || {};
309
+ const portfolioRelatedResearch = scanDocDir(meshDir, 'research');
310
+ const baseContext = {
311
+ scope: { level: opts.scope.level, bar_id: undefined, platform_id: undefined },
312
+ mesh_sha: meshSha,
313
+ portfolio: {
314
+ name: portfolio.name || 'unnamed-portfolio',
315
+ governance_policy: null, // phase 4: parse policies/*.yaml
316
+ capability_models: [], // phase 4: parse capability-models/
317
+ related_research_summaries: portfolioRelatedResearch.map(stripMtime),
318
+ },
319
+ platform: null,
320
+ bar: null,
321
+ };
322
+ // -------- platform / bar branches --------
323
+ let platformSlug = null;
324
+ let platformDir = null;
325
+ let barPath = null;
326
+ let barId;
327
+ if (opts.scope.level === 'bar') {
328
+ if (!opts.scope.id) {
329
+ throw new Error('scope.level=bar requires scope.id');
330
+ }
331
+ const found = findBarPath(meshDir, opts.scope.id);
332
+ if (!found) {
333
+ throw new Error(`Could not find BAR with id ${opts.scope.id} in ${meshDir}`);
334
+ }
335
+ barPath = found.barPath;
336
+ platformSlug = found.platformSlug;
337
+ platformDir = path.join(meshDir, 'platforms', platformSlug);
338
+ barId = opts.scope.id;
339
+ }
340
+ else {
341
+ // platform scope: scope.id is the slug
342
+ platformSlug = opts.scope.id;
343
+ platformDir = path.join(meshDir, 'platforms', platformSlug);
344
+ if (!fs.existsSync(platformDir)) {
345
+ throw new Error(`Could not find platform "${platformSlug}" in ${meshDir}/platforms/`);
346
+ }
347
+ }
348
+ const platformYaml = loadYamlFile(path.join(platformDir, 'platform.yaml')) || {};
349
+ const platformArch = loadJsonFile(path.join(platformDir, 'platform.arch.json'));
350
+ const platformResearch = scanDocDir(platformDir, 'research').map(stripMtime);
351
+ const platformId = platformYaml.id || `PLT-${platformSlug.toUpperCase()}`;
352
+ const ctx = {
353
+ ...baseContext,
354
+ scope: { level: opts.scope.level, bar_id: barId, platform_id: platformId },
355
+ platform: {
356
+ platform_id: platformId,
357
+ architecture: platformArch,
358
+ sibling_bars: listSiblingBars(platformDir, barId || ''),
359
+ related_research_summaries: platformResearch,
360
+ },
361
+ };
362
+ if (opts.scope.level === 'platform') {
363
+ return ctx;
364
+ }
365
+ // bar branch
366
+ const appYaml = loadYamlFile(path.join(barPath, 'app.yaml')) || {};
367
+ const app = appYaml.application ?? {};
368
+ const calmModel = loadJsonFile(path.join(barPath, 'architecture', 'bar.arch.json'));
369
+ const threatModelParsed = (0, threat_model_reader_1.readThreatModelFromBar)(barPath);
370
+ const adrs = readAdrs(barPath);
371
+ const research = scanDocDir(barPath, 'research');
372
+ const prds = scanDocDir(barPath, 'prds');
373
+ const meshGaps = detectMeshGapsFromDisk(barPath, research, adrs.length);
374
+ return {
375
+ ...ctx,
376
+ bar: {
377
+ bar_id: app.id || barId,
378
+ name: app.name || barId,
379
+ composite_score: 0, // phase 4 integration
380
+ tier: 'restricted', // phase 4 integration
381
+ calm_model: calmModel,
382
+ threats: threatModelParsed ? threatModelParsed.threats : null,
383
+ controls: null, // phase 4: parse security-controls.yaml
384
+ adrs,
385
+ pillar_scores: placeholderPillarScores(),
386
+ related_research: research.map(stripMtime),
387
+ related_prds: prds.map(stripMtime),
388
+ mesh_gaps: meshGaps,
389
+ linked_repos: extractLinkedRepos(appYaml),
390
+ },
391
+ };
392
+ }
@@ -0,0 +1,22 @@
1
+ export interface LoadedPrompt {
2
+ /** Path to the pack relative to the mesh root (for audit). */
3
+ packPath: string;
4
+ /** SHA-256 of the raw, pre-substitution prompt body. */
5
+ packSha256: string;
6
+ /** Body after `{placeholder}` substitution. */
7
+ filled: string;
8
+ /** Variables that the prompt asked for but weren't found in context. */
9
+ missingKeys: string[];
10
+ }
11
+ export interface LoadPromptOpts {
12
+ /** Mesh repo root. */
13
+ meshDir: string;
14
+ /** Pack id relative to `.caterpillar/prompts/`, e.g. "research/query-plan". */
15
+ packId: string;
16
+ /** Substitution context. Dotted keys are walked. */
17
+ context: Record<string, unknown>;
18
+ /** Inject the current year. Defaults to UTC now. */
19
+ now?: Date;
20
+ }
21
+ /** Read + hash + substitute a prompt pack. Throws if the pack file is missing. */
22
+ export declare function loadPrompt(opts: LoadPromptOpts): LoadedPrompt;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadPrompt = loadPrompt;
37
+ /**
38
+ * prompt-loader — reads a `.caterpillar/prompts/<pack>.md` file, hashes it
39
+ * for the audit log, and substitutes `{placeholder}` variables from a
40
+ * context object.
41
+ *
42
+ * Substitution semantics:
43
+ * `{flat}` → context.flat
44
+ * `{nested.path}` → context.nested.path (dot-walks)
45
+ * `{current_year}` → current 4-digit year (always injected)
46
+ * value types are rendered as:
47
+ * string/number/boolean → toString
48
+ * null/undefined → "(unset)"
49
+ * string[] → bullet list, one per line
50
+ * other arrays/objects → JSON.stringify with 2-space indent
51
+ *
52
+ * Unmatched `{tokens}` are left intact (so prompts can mention literal
53
+ * curly-brace content in code samples or YAML fences without surprise
54
+ * substitution; only top-level / dot-walked keys present in the context
55
+ * get replaced).
56
+ */
57
+ const node_crypto_1 = require("node:crypto");
58
+ const fs = __importStar(require("node:fs"));
59
+ const path = __importStar(require("node:path"));
60
+ const PROMPT_DIR = '.caterpillar/prompts';
61
+ /** Read + hash + substitute a prompt pack. Throws if the pack file is missing. */
62
+ function loadPrompt(opts) {
63
+ const relPath = `${PROMPT_DIR}/${opts.packId}.md`;
64
+ const absPath = path.join(opts.meshDir, relPath);
65
+ if (!fs.existsSync(absPath)) {
66
+ throw new Error(`Prompt pack not found at ${relPath} (looked under ${opts.meshDir}). Run "Refresh Prompts" in Looking Glass settings.`);
67
+ }
68
+ const raw = fs.readFileSync(absPath, 'utf8');
69
+ const sha256 = (0, node_crypto_1.createHash)('sha256').update(raw, 'utf8').digest('hex');
70
+ const augmented = {
71
+ current_year: (opts.now ?? new Date()).getUTCFullYear(),
72
+ ...opts.context,
73
+ };
74
+ const missingKeys = [];
75
+ const filled = raw.replace(/\{([a-zA-Z_][\w.]*)\}/g, (match, key) => {
76
+ const value = walk(augmented, key);
77
+ if (value === undefined) {
78
+ missingKeys.push(key);
79
+ return match;
80
+ }
81
+ return renderValue(value);
82
+ });
83
+ return { packPath: relPath, packSha256: sha256, filled, missingKeys };
84
+ }
85
+ function walk(ctx, key) {
86
+ const parts = key.split('.');
87
+ let cur = ctx;
88
+ for (const part of parts) {
89
+ if (cur == null || typeof cur !== 'object') {
90
+ return undefined;
91
+ }
92
+ cur = cur[part];
93
+ }
94
+ return cur;
95
+ }
96
+ function renderValue(value) {
97
+ if (value === null || value === undefined) {
98
+ return '(unset)';
99
+ }
100
+ if (typeof value === 'string') {
101
+ return value;
102
+ }
103
+ if (typeof value === 'number' || typeof value === 'boolean') {
104
+ return String(value);
105
+ }
106
+ if (Array.isArray(value)) {
107
+ if (value.length === 0) {
108
+ return '(none)';
109
+ }
110
+ if (value.every(v => typeof v === 'string')) {
111
+ return value.map(v => `- ${v}`).join('\n');
112
+ }
113
+ return '```json\n' + JSON.stringify(value, null, 2) + '\n```';
114
+ }
115
+ if (typeof value === 'object') {
116
+ return '```json\n' + JSON.stringify(value, null, 2) + '\n```';
117
+ }
118
+ return String(value);
119
+ }
@@ -0,0 +1,33 @@
1
+ export type StrideCategory = 'spoofing' | 'tampering' | 'repudiation' | 'information-disclosure' | 'denial-of-service' | 'elevation-of-privilege';
2
+ export type ImpactLevel = 'critical' | 'high' | 'medium' | 'low';
3
+ export type LikelihoodLevel = 'high' | 'medium' | 'low';
4
+ export type ControlEffectiveness = 'full' | 'partial' | 'none';
5
+ export type ResidualRisk = 'critical' | 'high' | 'medium' | 'low' | 'negligible';
6
+ export interface ThreatEntry {
7
+ id: string;
8
+ category: StrideCategory;
9
+ target: string;
10
+ targetName: string;
11
+ dataClassification: string;
12
+ description: string;
13
+ attackVector: string;
14
+ impact: ImpactLevel;
15
+ likelihood: LikelihoodLevel;
16
+ existingControls: string[];
17
+ controlEffectiveness: ControlEffectiveness;
18
+ residualRisk: ResidualRisk;
19
+ recommendedMitigations: string[];
20
+ nistReferences: string[];
21
+ }
22
+ export interface ThreatModelSummary {
23
+ byCategory: Record<string, number>;
24
+ byRisk: Record<string, number>;
25
+ unmitigatedCount: number;
26
+ }
27
+ export interface ParsedThreatModel {
28
+ threats: ThreatEntry[];
29
+ summary: ThreatModelSummary;
30
+ }
31
+ export declare function parseThreatModelYaml(content: string): ThreatEntry[];
32
+ export declare function summarizeThreatModel(threats: ThreatEntry[]): ThreatModelSummary;
33
+ export declare function readThreatModelFromBar(barPath: string): ParsedThreatModel | null;