@kylewadegrove/cutline-mcp-cli 0.6.0 → 0.6.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.
@@ -5,12 +5,6 @@ import {
5
5
  computeMetricsFromGraph,
6
6
  detectConstraintConflicts
7
7
  } from "./chunk-UBBAYTW3.js";
8
- import {
9
- recordScoreSnapshot
10
- } from "./chunk-TGSEURMN.js";
11
- import {
12
- firestoreRetry
13
- } from "./chunk-PU7TL6S3.js";
14
8
  import {
15
9
  isWriteTool
16
10
  } from "./chunk-KMUSQOTJ.js";
@@ -29,21 +23,60 @@ import {
29
23
  listPersonas,
30
24
  saveWikiMarkdown,
31
25
  uploadAndSign
32
- } from "./chunk-IVWF7VYZ.js";
26
+ } from "./chunk-M37M2UA4.js";
27
+ import "./chunk-JBJYSV4P.js";
33
28
  import {
34
- getStoredToken,
35
29
  guardBoundary,
36
30
  guardOutput,
37
- initFirebase,
38
- mapErrorToMcp,
39
31
  perfTracker,
32
+ withPerfTracking
33
+ } from "./chunk-OP4EO6FV.js";
34
+ import {
35
+ addEdges,
36
+ addEntity,
37
+ addNodes,
38
+ addTestCases,
39
+ createPremortem,
40
+ createTemplate,
41
+ generateReadinessReportViaProxy,
42
+ getAllBindings,
43
+ getAllEdges,
44
+ getAllEntities,
45
+ getAllNodes,
46
+ getAllNodesLight,
47
+ getEntitiesWithEmbeddings,
48
+ getGraphMetadata,
49
+ getIdeaReport,
50
+ getNodesByCategories,
51
+ getNodesMissingEmbeddings,
52
+ getNodesWithEmbeddings,
53
+ getPremortem,
54
+ getScanRateLimit,
55
+ getTemplate,
56
+ getTestCasesForEntity,
57
+ hasConstraints,
58
+ listPremortems,
59
+ listTemplates,
60
+ recordScoreSnapshot,
61
+ saveScanReport,
62
+ updateEntityEmbedding,
63
+ updateGraphMetadata,
64
+ updateNodeEmbeddings,
65
+ updatePremortem,
66
+ updateScanRateLimit,
67
+ updateTemplate,
68
+ upsertBindings,
69
+ upsertEdges,
70
+ upsertEntities,
71
+ upsertNodes
72
+ } from "./chunk-PQUAX5YW.js";
73
+ import {
74
+ mapErrorToMcp,
75
+ requirePremiumWithAutoAuth,
40
76
  resolveAuthContext,
41
77
  resolveAuthContextFree,
42
- validateAuth,
43
- validateRequestSize,
44
- validateSubscription,
45
- withPerfTracking
46
- } from "./chunk-PD2HN2R5.js";
78
+ validateRequestSize
79
+ } from "./chunk-NUBIEJTU.js";
47
80
  import {
48
81
  PMJsonSchema,
49
82
  RunInputSchema,
@@ -52,170 +85,11 @@ import {
52
85
  runRolesAndSummarize,
53
86
  runRolesAndSummarizeIncremental
54
87
  } from "./chunk-7FHM2GD3.js";
55
- import "./chunk-JBJYSV4P.js";
56
88
 
57
89
  // ../mcp/dist/mcp/src/cutline-server.js
58
90
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
59
91
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
60
92
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
61
- import admin from "firebase-admin";
62
- import { getApps as getApps4, getApp as getApp4 } from "firebase-admin/app";
63
- import { getFirestore as getFirestore4 } from "firebase-admin/firestore";
64
-
65
- // ../mcp/dist/mcp/src/context-graph/graph-store.js
66
- import { getApps, getApp } from "firebase-admin/app";
67
- import { getFirestore, Timestamp } from "firebase-admin/firestore";
68
- function getNodesCollection(productId) {
69
- const app = getApps().length > 0 ? getApp() : void 0;
70
- if (!app)
71
- throw new Error("Firebase not initialized");
72
- const db2 = getFirestore(app);
73
- return db2.collection("context_graph").doc(productId).collection("nodes");
74
- }
75
- var NODE_CACHE_TTL = 10 * 60 * 1e3;
76
- var nodeCache = /* @__PURE__ */ new Map();
77
- var lightNodeCache = /* @__PURE__ */ new Map();
78
- function invalidateNodeCache(productId) {
79
- nodeCache.delete(productId);
80
- lightNodeCache.delete(productId);
81
- }
82
- var LIGHT_FIELDS = [
83
- "category",
84
- "summary",
85
- "keywords",
86
- "severity",
87
- "action",
88
- "source_type",
89
- "source_id",
90
- "ingested_at",
91
- "confidence",
92
- "threshold",
93
- "conditions",
94
- "scope",
95
- "phase"
96
- ];
97
- async function upsertNodes(productId, sourceType, sourceId, nodes) {
98
- const collection = getNodesCollection(productId);
99
- const db2 = getFirestore(getApp());
100
- const batch = db2.batch();
101
- const existingQuery = await collection.where("source_type", "==", sourceType).where("source_id", "==", sourceId).get();
102
- existingQuery.docs.forEach((doc) => {
103
- batch.delete(doc.ref);
104
- });
105
- for (const node of nodes) {
106
- const docRef = collection.doc(node.id);
107
- const nodeDoc = {
108
- ...node,
109
- ingested_at: Timestamp.fromDate(node.ingested_at)
110
- };
111
- const cleaned = Object.fromEntries(Object.entries(nodeDoc).filter(([, v]) => v !== void 0));
112
- batch.set(docRef, cleaned);
113
- }
114
- await firestoreRetry(() => batch.commit(), { operationName: "upsert_nodes" });
115
- invalidateNodeCache(productId);
116
- }
117
- async function getAllNodes(productId) {
118
- const cached = nodeCache.get(productId);
119
- if (cached && Date.now() - cached.fetchedAt < NODE_CACHE_TTL) {
120
- return cached.nodes;
121
- }
122
- const collection = getNodesCollection(productId);
123
- const snapshot = await collection.get();
124
- const nodes = snapshot.docs.map((doc) => {
125
- const data = doc.data();
126
- return {
127
- ...data,
128
- id: doc.id,
129
- ingested_at: data.ingested_at.toDate()
130
- };
131
- });
132
- nodeCache.set(productId, { nodes, fetchedAt: Date.now() });
133
- return nodes;
134
- }
135
- async function getAllNodesLight(productId) {
136
- const cached = lightNodeCache.get(productId);
137
- if (cached && Date.now() - cached.fetchedAt < NODE_CACHE_TTL) {
138
- return cached.nodes;
139
- }
140
- const collection = getNodesCollection(productId);
141
- const snapshot = await collection.select(...LIGHT_FIELDS).get();
142
- const nodes = snapshot.docs.map((doc) => {
143
- const data = doc.data();
144
- return {
145
- ...data,
146
- id: doc.id,
147
- ingested_at: data.ingested_at?.toDate() ?? /* @__PURE__ */ new Date()
148
- };
149
- });
150
- lightNodeCache.set(productId, { nodes, fetchedAt: Date.now() });
151
- return nodes;
152
- }
153
- async function getNodesByCategories(productId, categories) {
154
- const collection = getNodesCollection(productId);
155
- const snapshot = await collection.where("category", "in", categories.slice(0, 10)).get();
156
- return snapshot.docs.map((doc) => {
157
- const data = doc.data();
158
- return {
159
- ...data,
160
- id: doc.id,
161
- ingested_at: data.ingested_at.toDate()
162
- };
163
- });
164
- }
165
- async function hasConstraints(productId) {
166
- const collection = getNodesCollection(productId);
167
- const snapshot = await collection.limit(1).get();
168
- return !snapshot.empty;
169
- }
170
- async function addNodes(productId, nodes) {
171
- if (nodes.length === 0)
172
- return;
173
- const collection = getNodesCollection(productId);
174
- const db2 = collection.firestore;
175
- const MAX_BATCH = 490;
176
- for (let i = 0; i < nodes.length; i += MAX_BATCH) {
177
- const batch = db2.batch();
178
- for (const node of nodes.slice(i, i + MAX_BATCH)) {
179
- const ref = collection.doc(node.id);
180
- const nodeDoc = { ...node };
181
- if (node.ingested_at instanceof Date) {
182
- nodeDoc.ingested_at = Timestamp.fromDate(node.ingested_at);
183
- }
184
- Object.keys(nodeDoc).forEach((k) => {
185
- if (nodeDoc[k] === void 0)
186
- delete nodeDoc[k];
187
- });
188
- batch.set(ref, nodeDoc, { merge: true });
189
- }
190
- await firestoreRetry(() => batch.commit(), { operationName: "add_nodes" });
191
- }
192
- invalidateNodeCache(productId);
193
- }
194
- async function getNodesWithEmbeddings(productId) {
195
- const allNodes = await getAllNodes(productId);
196
- return allNodes.filter((node) => node.embedding && node.embedding.length > 0);
197
- }
198
- async function updateNodeEmbeddings(productId, embeddingUpdates) {
199
- if (embeddingUpdates.length === 0)
200
- return;
201
- const collection = getNodesCollection(productId);
202
- const db2 = getFirestore(getApp());
203
- const batchSize = 10;
204
- for (let i = 0; i < embeddingUpdates.length; i += batchSize) {
205
- const batch = db2.batch();
206
- const updates = embeddingUpdates.slice(i, i + batchSize);
207
- for (const { id, embedding } of updates) {
208
- const docRef = collection.doc(id);
209
- batch.update(docRef, { embedding });
210
- }
211
- await firestoreRetry(() => batch.commit(), { operationName: "update_node_embeddings" });
212
- }
213
- invalidateNodeCache(productId);
214
- }
215
- async function getNodesMissingEmbeddings(productId) {
216
- const allNodes = await getAllNodes(productId);
217
- return allNodes.filter((node) => !node.embedding || node.embedding.length === 0);
218
- }
219
93
 
220
94
  // ../mcp/dist/mcp/src/context-graph/embeddings.js
221
95
  import { GoogleAuth } from "google-auth-library";
@@ -1993,147 +1867,6 @@ async function extractConstraintsFromDoc(docId, text, generateFn, options = {})
1993
1867
  return convertExtractedToNodes(docId, extracted, options);
1994
1868
  }
1995
1869
 
1996
- // ../mcp/dist/mcp/src/context-graph/graph-store-v2.js
1997
- import { getApps as getApps2, getApp as getApp2 } from "firebase-admin/app";
1998
- import { getFirestore as getFirestore2, Timestamp as Timestamp2 } from "firebase-admin/firestore";
1999
- function db() {
2000
- const app = getApps2().length > 0 ? getApp2() : void 0;
2001
- if (!app)
2002
- throw new Error("Firebase not initialized");
2003
- return getFirestore2(app);
2004
- }
2005
- function productRoot(productId) {
2006
- return db().collection("context_graph").doc(productId);
2007
- }
2008
- function entitiesCol(productId) {
2009
- return productRoot(productId).collection("entities");
2010
- }
2011
- function edgesCol(productId) {
2012
- return productRoot(productId).collection("edges");
2013
- }
2014
- function bindingsCol(productId) {
2015
- return productRoot(productId).collection("bindings");
2016
- }
2017
- function testCasesCol(productId) {
2018
- return productRoot(productId).collection("test_cases");
2019
- }
2020
- function metaDoc(productId) {
2021
- return productRoot(productId).collection("_meta").doc("graph");
2022
- }
2023
- function toEntityDoc(e) {
2024
- return { ...e, ingested_at: Timestamp2.fromDate(e.ingested_at) };
2025
- }
2026
- function fromEntityDoc(id, data) {
2027
- return { ...data, id, ingested_at: data.ingested_at.toDate() };
2028
- }
2029
- var BATCH_LIMIT = 500;
2030
- async function commitInBatches(ops) {
2031
- for (let i = 0; i < ops.length; i += BATCH_LIMIT) {
2032
- await firestoreRetry(() => {
2033
- const batch = db().batch();
2034
- ops.slice(i, i + BATCH_LIMIT).forEach((op) => op(batch));
2035
- return batch.commit();
2036
- }, { operationName: "batch_commit" });
2037
- }
2038
- }
2039
- async function upsertEntities(productId, sourceType, sourceId, entities) {
2040
- const col = entitiesCol(productId);
2041
- const existing = await col.where("source_type", "==", sourceType).where("source_id", "==", sourceId).get();
2042
- const ops = [];
2043
- existing.docs.forEach((doc) => {
2044
- ops.push((b) => b.delete(doc.ref));
2045
- });
2046
- for (const entity of entities) {
2047
- const ref = col.doc(entity.id);
2048
- const doc = toEntityDoc(entity);
2049
- ops.push((b) => b.set(ref, doc));
2050
- }
2051
- await commitInBatches(ops);
2052
- }
2053
- async function getAllEntities(productId) {
2054
- const snap = await entitiesCol(productId).get();
2055
- return snap.docs.map((d) => fromEntityDoc(d.id, d.data()));
2056
- }
2057
- async function addEntity(productId, entity) {
2058
- const ref = entitiesCol(productId).doc(entity.id);
2059
- await firestoreRetry(() => ref.set(toEntityDoc(entity), { merge: true }), { operationName: "entity_set" });
2060
- }
2061
- async function updateEntityEmbedding(productId, entityId, embedding) {
2062
- await firestoreRetry(() => entitiesCol(productId).doc(entityId).update({ embedding }), { operationName: "entity_update_embedding" });
2063
- }
2064
- async function getEntitiesWithEmbeddings(productId) {
2065
- const all = await getAllEntities(productId);
2066
- return all.filter((e) => e.embedding && e.embedding.length > 0);
2067
- }
2068
- async function upsertEdges(productId, sourceId, edges) {
2069
- const col = edgesCol(productId);
2070
- const existing = await col.get();
2071
- const ops = [];
2072
- existing.docs.forEach((doc) => {
2073
- if (doc.id.startsWith(sourceId)) {
2074
- ops.push((b) => b.delete(doc.ref));
2075
- }
2076
- });
2077
- for (const edge of edges) {
2078
- const ref = col.doc(edge.id);
2079
- ops.push((b) => b.set(ref, edge));
2080
- }
2081
- await commitInBatches(ops);
2082
- }
2083
- async function getAllEdges(productId) {
2084
- const snap = await edgesCol(productId).get();
2085
- return snap.docs.map((d) => ({ ...d.data(), id: d.id }));
2086
- }
2087
- async function addEdges(productId, edges) {
2088
- const col = edgesCol(productId);
2089
- const ops = [];
2090
- for (const edge of edges) {
2091
- const ref = col.doc(edge.id);
2092
- ops.push((b) => b.set(ref, edge, { merge: true }));
2093
- }
2094
- await commitInBatches(ops);
2095
- }
2096
- async function upsertBindings(productId, bindings) {
2097
- const col = bindingsCol(productId);
2098
- const ops = [];
2099
- for (const binding of bindings) {
2100
- const ref = col.doc(binding.id);
2101
- ops.push((b) => b.set(ref, binding, { merge: true }));
2102
- }
2103
- await commitInBatches(ops);
2104
- }
2105
- async function getAllBindings(productId) {
2106
- const snap = await bindingsCol(productId).get();
2107
- return snap.docs.map((d) => ({ ...d.data(), id: d.id }));
2108
- }
2109
- async function addTestCases(productId, cases) {
2110
- const col = testCasesCol(productId);
2111
- const ops = [];
2112
- for (const tc of cases) {
2113
- const ref = col.doc(tc.id);
2114
- ops.push((b) => b.set(ref, tc, { merge: true }));
2115
- }
2116
- await commitInBatches(ops);
2117
- }
2118
- async function getTestCasesForEntity(productId, entityId) {
2119
- const snap = await testCasesCol(productId).where("entity_id", "==", entityId).get();
2120
- return snap.docs.map((d) => ({ ...d.data(), id: d.id }));
2121
- }
2122
- async function updateGraphMetadata(productId, meta) {
2123
- const payload = { ...meta };
2124
- if (meta.last_build) {
2125
- payload.last_build = Timestamp2.fromDate(meta.last_build);
2126
- }
2127
- await firestoreRetry(() => metaDoc(productId).set(payload, { merge: true }), { operationName: "graph_metadata_set" });
2128
- }
2129
- async function getGraphMetadata(productId) {
2130
- const doc = await metaDoc(productId).get();
2131
- if (!doc.exists)
2132
- return null;
2133
- const data = doc.data();
2134
- return { ...data, last_build: data.last_build?.toDate?.() ?? /* @__PURE__ */ new Date() };
2135
- }
2136
-
2137
1870
  // ../mcp/dist/mcp/src/adapters/requirements-adapter.js
2138
1871
  var EXTRACTION_SYSTEM_PROMPT2 = `You are a requirements graph extractor. Given product requirements (PRD, user stories, specs), extract a structured graph of entities, relationships, and non-functional requirements.
2139
1872
 
@@ -2918,8 +2651,6 @@ function pct(value) {
2918
2651
  }
2919
2652
 
2920
2653
  // ../mcp/dist/mcp/src/context-graph/readiness-report.js
2921
- import { getApp as getApp3, getApps as getApps3 } from "firebase-admin/app";
2922
- import { getFirestore as getFirestore3 } from "firebase-admin/firestore";
2923
2654
  function computeGrade(pct2) {
2924
2655
  if (pct2 >= 90)
2925
2656
  return "A+";
@@ -2931,76 +2662,6 @@ function computeGrade(pct2) {
2931
2662
  return "C";
2932
2663
  return "D";
2933
2664
  }
2934
- function reportIdFromProductId(productId) {
2935
- return productId;
2936
- }
2937
- async function generateReadinessReport(input) {
2938
- const app = getApps3().length > 0 ? getApp3() : void 0;
2939
- if (!app)
2940
- throw new Error("Firebase not initialized");
2941
- const db2 = getFirestore3(app);
2942
- const { productId, ownerUid } = input;
2943
- const [entities, edges, constraintNodes, bindings] = await Promise.all([
2944
- getAllEntities(productId),
2945
- getAllEdges(productId),
2946
- getAllNodes(productId),
2947
- getAllBindings(productId)
2948
- ]);
2949
- if (entities.length === 0 && constraintNodes.length === 0) {
2950
- throw new Error(`No graph data for product "${productId}". Run graph_ingest_requirements or constraints_ingest first.`);
2951
- }
2952
- const metrics = computeMetricsFromGraph(entities, edges, constraintNodes, bindings);
2953
- const readinessPct = metrics.engineering_readiness_pct ?? 0;
2954
- const jobSnap = await db2.collection("premortem_jobs").doc(productId).get();
2955
- const jobData = jobSnap.exists ? jobSnap.data() : void 0;
2956
- const result = jobData?.result;
2957
- const productName = result?.project?.name || jobData?.payload?.project?.name || "Untitled Product";
2958
- const verdict = result?.decision?.recommendation;
2959
- const techStack = entities.filter((e) => e.type === "component").map((e) => e.name).slice(0, 15);
2960
- const reportId = reportIdFromProductId(productId);
2961
- const now = /* @__PURE__ */ new Date();
2962
- const report = {
2963
- productId,
2964
- productName,
2965
- ownerUid,
2966
- grade: computeGrade(readinessPct),
2967
- readinessPct: Math.round(readinessPct),
2968
- securityReadinessPct: Math.round(metrics.security_readiness_pct ?? 0),
2969
- reliabilityReadinessPct: Math.round(metrics.reliability_readiness_pct ?? 0),
2970
- scalabilityReadinessPct: Math.round(metrics.scalability_readiness_pct ?? 0),
2971
- nfrCoverage: metrics.nfr_coverage,
2972
- timeEstimate: {
2973
- unassisted_hours: metrics.time_estimate.unassisted_hours,
2974
- assisted_hours: metrics.time_estimate.assisted_hours,
2975
- speedup_factor: metrics.time_estimate.speedup_factor
2976
- },
2977
- complexityFactors: {
2978
- entity_count: metrics.complexity_factors.entity_count,
2979
- nfr_count: metrics.complexity_factors.nfr_count,
2980
- critical_nfr_count: metrics.complexity_factors.critical_nfr_count,
2981
- pii_data_types: metrics.complexity_factors.pii_data_types,
2982
- conflict_count: metrics.complexity_factors.conflict_count
2983
- },
2984
- rgrCompletedPhases: metrics.rgr_completed_phases ?? [],
2985
- complianceFrameworks: metrics.compliance_frameworks ?? [],
2986
- verdict,
2987
- techStack,
2988
- public: true,
2989
- createdAt: now,
2990
- updatedAt: now
2991
- };
2992
- const docRef = db2.collection("readiness_reports").doc(reportId);
2993
- const existingSnap = await docRef.get();
2994
- if (existingSnap.exists) {
2995
- report.createdAt = existingSnap.data()?.createdAt?.toDate?.() ?? now;
2996
- }
2997
- await docRef.set({
2998
- ...report,
2999
- createdAt: report.createdAt,
3000
- updatedAt: now
3001
- });
3002
- return { report, reportId };
3003
- }
3004
2665
  var SITE_URL = "https://thecutline.ai";
3005
2666
  function buildReportMarkdown(report, reportId) {
3006
2667
  const cov = report.nfrCoverage;
@@ -8918,7 +8579,6 @@ What's your connection to this space? Are you an insider who knows the pain, or
8918
8579
  throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
8919
8580
  }
8920
8581
  session.conversationHistory.push({ role: "user", content: message });
8921
- initFirebase();
8922
8582
  const context = {
8923
8583
  currentAct: session.currentAct,
8924
8584
  initialInput: session.initialInput,
@@ -9050,14 +8710,8 @@ What's your connection to this space? Are you an insider who knows the pain, or
9050
8710
  const valueScore = Math.round(idea.painIntensity * idea.aiLeverage * idea.marketAccessibility / 10);
9051
8711
  let isPremium = false;
9052
8712
  try {
9053
- const storedToken = await getStoredToken();
9054
- if (storedToken) {
9055
- initFirebase(storedToken.environment);
9056
- const decoded2 = await validateAuth(storedToken.refreshToken ? void 0 : storedToken.refreshToken);
9057
- if (decoded2?.uid) {
9058
- isPremium = await validateSubscription(decoded2.uid);
9059
- }
9060
- }
8713
+ await requirePremiumWithAutoAuth();
8714
+ isPremium = true;
9061
8715
  } catch (e) {
9062
8716
  }
9063
8717
  if (!isPremium) {
@@ -9135,7 +8789,6 @@ Why AI: ${idea.whyAI}`
9135
8789
  }
9136
8790
  if (name2 === "trial_generate") {
9137
8791
  const { prompt } = args;
9138
- initFirebase();
9139
8792
  const text = await generateTrialRun(prompt);
9140
8793
  return { content: [{ type: "text", text: JSON.stringify({ text }) }] };
9141
8794
  }
@@ -9146,27 +8799,20 @@ Why AI: ${idea.whyAI}`
9146
8799
  }
9147
8800
  const freeAuth = await resolveAuthContextFree(scanArgs.auth_token);
9148
8801
  const uid = freeAuth.effectiveUid;
9149
- initFirebase();
9150
- const rateLimitApp = getApps4().length > 0 ? getApp4() : void 0;
9151
- if (rateLimitApp) {
9152
- const rateLimitDb = getFirestore4(rateLimitApp);
9153
- const profileRef = rateLimitDb.collection("users").doc(uid);
9154
- const profileSnap = await profileRef.get();
9155
- const profile = profileSnap.data() ?? {};
9156
- const scanCounter = profile.free_scan_counter ?? { count: 0, reset_at: null };
8802
+ {
8803
+ const rateInfo = await getScanRateLimit();
9157
8804
  const now = /* @__PURE__ */ new Date();
9158
- const resetAt = scanCounter.reset_at?.toDate?.() ?? /* @__PURE__ */ new Date(0);
8805
+ const resetAt = rateInfo.periodStart ? new Date(rateInfo.periodStart) : /* @__PURE__ */ new Date(0);
9159
8806
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
9160
8807
  if (resetAt < monthStart) {
9161
- await firestoreRetry(() => profileRef.set({ free_scan_counter: { count: 0, reset_at: now } }, { merge: true }), { operationName: "reset_scan_counter" });
9162
- scanCounter.count = 0;
9163
- } else if (scanCounter.count >= 3) {
9164
- const hasPremium = await validateSubscription(uid);
9165
- if (!hasPremium) {
8808
+ await updateScanRateLimit({ free_scan_counter: { count: 1, reset_at: now.toISOString() } });
8809
+ } else if (rateInfo.scanCount >= 3) {
8810
+ if (rateInfo.subscription !== "active" && rateInfo.subscription !== "trialing") {
9166
8811
  throw new McpError(ErrorCode.InvalidRequest, "Free scan limit reached (3/month). Run `cutline-mcp upgrade` in your terminal, or visit https://thecutline.ai/upgrade");
9167
8812
  }
8813
+ } else {
8814
+ await updateScanRateLimit({ free_scan_counter: { count: rateInfo.scanCount + 1, reset_at: rateInfo.periodStart ?? now.toISOString() } });
9168
8815
  }
9169
- await firestoreRetry(() => profileRef.set({ free_scan_counter: { count: admin.firestore.FieldValue.increment(1), reset_at: scanCounter.reset_at ?? now } }, { merge: true }), { operationName: "increment_scan_counter" });
9170
8816
  }
9171
8817
  const genericGraphId = buildGenericGraphId(uid, scanArgs.project_root);
9172
8818
  const result = await handleSecurityScan(genericGraphId, uid, scanArgs, {
@@ -9182,44 +8828,36 @@ Why AI: ${idea.whyAI}`
9182
8828
  updateGraphMetadata: (pid, patch) => updateGraphMetadata(pid, patch)
9183
8829
  });
9184
8830
  try {
9185
- const { recordScoreSnapshot: recordScoreSnapshot2 } = await import("./score-history-HO5KRVGC.js");
9186
- await recordScoreSnapshot2(genericGraphId, result.metrics, "engineering_audit");
8831
+ await recordScoreSnapshot(genericGraphId, result.metrics, "engineering_audit");
9187
8832
  } catch (e) {
9188
8833
  console.error("[engineering_audit] Score snapshot failed (non-fatal):", e);
9189
8834
  }
9190
8835
  let reportId;
9191
8836
  try {
9192
- const app = getApps4().length > 0 ? getApp4() : void 0;
9193
- if (app) {
9194
- const db2 = getFirestore4(app);
9195
- const reportRef = db2.collection("scan_reports").doc();
9196
- reportId = reportRef.id;
9197
- await firestoreRetry(() => reportRef.set({
9198
- uid,
9199
- created_at: admin.firestore.FieldValue.serverTimestamp(),
9200
- metrics: {
9201
- engineering_readiness_pct: result.metrics.engineering_readiness_pct ?? 0,
9202
- security_readiness_pct: result.metrics.security_readiness_pct ?? 0,
9203
- reliability_readiness_pct: result.metrics.reliability_readiness_pct ?? 0,
9204
- scalability_readiness_pct: result.metrics.scalability_readiness_pct ?? 0
9205
- },
9206
- ecosystem: result.ecosystem,
9207
- binding_coverage_pct: result.bindingHealth.coverage_pct,
9208
- frameworks_loaded: result.frameworksLoaded,
9209
- sensitive_data_count: result.sensitiveDataCount,
9210
- findings: result.gatedGapDetails.map((f) => ({
9211
- title: f.title,
9212
- severity: f.severity,
9213
- category: f.category,
9214
- description: f.description
9215
- })),
9216
- sca_summary: result.scaFindings ? {
9217
- total: result.scaFindings.total,
9218
- critical: result.scaFindings.critical,
9219
- high: result.scaFindings.high
9220
- } : null
9221
- }), { operationName: "persist_scan_report" });
9222
- }
8837
+ const saved = await saveScanReport({
8838
+ metrics: {
8839
+ engineering_readiness_pct: result.metrics.engineering_readiness_pct ?? 0,
8840
+ security_readiness_pct: result.metrics.security_readiness_pct ?? 0,
8841
+ reliability_readiness_pct: result.metrics.reliability_readiness_pct ?? 0,
8842
+ scalability_readiness_pct: result.metrics.scalability_readiness_pct ?? 0
8843
+ },
8844
+ ecosystem: result.ecosystem,
8845
+ binding_coverage_pct: result.bindingHealth.coverage_pct,
8846
+ frameworks_loaded: result.frameworksLoaded,
8847
+ sensitive_data_count: result.sensitiveDataCount,
8848
+ findings: result.gatedGapDetails.map((f) => ({
8849
+ title: f.title,
8850
+ severity: f.severity,
8851
+ category: f.category,
8852
+ description: f.description
8853
+ })),
8854
+ sca_summary: result.scaFindings ? {
8855
+ total: result.scaFindings.total,
8856
+ critical: result.scaFindings.critical,
8857
+ high: result.scaFindings.high
8858
+ } : null
8859
+ });
8860
+ reportId = saved.id;
9223
8861
  } catch (e) {
9224
8862
  console.error("[engineering_audit] Report persistence failed (non-fatal):", e);
9225
8863
  }
@@ -9237,42 +8875,25 @@ Why AI: ${idea.whyAI}`
9237
8875
  case "premortem_run": {
9238
8876
  const { input } = args;
9239
8877
  const parsedInput = RunInputSchema.parse(input);
9240
- initFirebase();
9241
- const runApp = getApps4().length > 0 ? getApp4() : void 0;
9242
- if (!runApp)
9243
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9244
- const runDb = getFirestore4(runApp);
9245
- const runDocRef = runDb.collection("premortem_jobs").doc();
9246
- if (!parsedInput.productId) {
9247
- parsedInput.productId = runDocRef.id;
9248
- }
9249
- await firestoreRetry(() => runDocRef.set({
9250
- status: "running",
9251
- payload: parsedInput,
9252
- createdAt: admin.firestore.FieldValue.serverTimestamp(),
9253
- uid: effectiveUid || null
9254
- }), { operationName: "premortem_run_set" });
8878
+ const jobId = parsedInput.productId || `pm_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
8879
+ if (!parsedInput.productId)
8880
+ parsedInput.productId = jobId;
8881
+ const { id: runDocId } = await createPremortem({ status: "running", payload: parsedInput }, jobId);
9255
8882
  const timeoutMs = 10 * 60 * 1e3;
9256
8883
  const result = await Promise.race([
9257
8884
  runRolesAndSummarize(parsedInput, { includeDiagnostics: true }),
9258
8885
  new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
9259
8886
  ]);
9260
- await firestoreRetry(() => runDocRef.update({ status: "completed", result, finishedAt: admin.firestore.FieldValue.serverTimestamp() }), { operationName: "premortem_run_complete" });
9261
- return { content: [{ type: "text", text: JSON.stringify({ ...result, _jobId: runDocRef.id, _graphProductId: parsedInput.productId }) }] };
8887
+ await updatePremortem(runDocId, { status: "completed", result });
8888
+ return { content: [{ type: "text", text: JSON.stringify({ ...result, _jobId: runDocId, _graphProductId: parsedInput.productId }) }] };
9262
8889
  }
9263
8890
  case "premortem_from_idea": {
9264
8891
  const { idea_report_id } = args;
9265
8892
  if (!idea_report_id)
9266
8893
  throw new McpError(ErrorCode.InvalidParams, "idea_report_id is required");
9267
- initFirebase();
9268
- const ftApp = getApps4().length > 0 ? getApp4() : void 0;
9269
- if (!ftApp)
9270
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9271
- const ftDb = getFirestore4(ftApp);
9272
- const ideaSnap = await firestoreRetry(() => ftDb.collection("idea_reports").doc(idea_report_id).get(), { operationName: "premortem_from_idea_read" });
9273
- if (!ideaSnap.exists)
8894
+ const ideaData = await getIdeaReport(idea_report_id);
8895
+ if (!ideaData)
9274
8896
  throw new McpError(ErrorCode.InvalidRequest, `Idea report not found: ${idea_report_id}`);
9275
- const ideaData = ideaSnap.data();
9276
8897
  const risks = (ideaData.risks ?? []).map((r) => r.title).join(", ");
9277
8898
  const assumptions = (ideaData.assumptions ?? []).map((a) => a.title).join(", ");
9278
8899
  const competitors = (ideaData.competitive_threats ?? []).map((t) => t.threat).join(", ");
@@ -9301,32 +8922,25 @@ Competitive threats: ${competitors}` : ""
9301
8922
  competitors: (ideaData.competitive_threats ?? []).map((t) => t.threat)
9302
8923
  }
9303
8924
  });
9304
- const ftDocRef = ftDb.collection("premortem_jobs").doc();
9305
- ftInput.productId = ftDocRef.id;
9306
- await firestoreRetry(() => ftDocRef.set({
8925
+ const { id: ftDocId } = await createPremortem({
9307
8926
  status: "running",
9308
8927
  payload: ftInput,
9309
8928
  source: "fast_track_idea",
9310
- idea_report_id,
9311
- createdAt: admin.firestore.FieldValue.serverTimestamp(),
9312
- uid: effectiveUid || null
9313
- }), { operationName: "premortem_from_idea_set" });
8929
+ idea_report_id
8930
+ });
8931
+ ftInput.productId = ftDocId;
9314
8932
  const ftTimeoutMs = 10 * 60 * 1e3;
9315
8933
  const ftResult = await Promise.race([
9316
8934
  runRolesAndSummarize(ftInput, { includeDiagnostics: true }),
9317
8935
  new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ftTimeoutMs))
9318
8936
  ]);
9319
- await firestoreRetry(() => ftDocRef.update({
9320
- status: "completed",
9321
- result: ftResult,
9322
- finishedAt: admin.firestore.FieldValue.serverTimestamp()
9323
- }), { operationName: "premortem_from_idea_complete" });
8937
+ await updatePremortem(ftDocId, { status: "completed", result: ftResult });
9324
8938
  return {
9325
8939
  content: [{
9326
8940
  type: "text",
9327
8941
  text: JSON.stringify({
9328
8942
  ...ftResult,
9329
- _jobId: ftDocRef.id,
8943
+ _jobId: ftDocId,
9330
8944
  _graphProductId: ftInput.productId,
9331
8945
  _source: "fast_track_idea",
9332
8946
  _idea_report_id: idea_report_id,
@@ -9338,32 +8952,16 @@ Competitive threats: ${competitors}` : ""
9338
8952
  case "premortem_queue": {
9339
8953
  const { input } = args;
9340
8954
  const parsedInput = RunInputSchema.parse(input);
9341
- const app = getApps4().length > 0 ? getApp4() : void 0;
9342
- if (!app)
9343
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9344
- const db2 = getFirestore4(app);
9345
- const docRef = db2.collection("premortem_jobs").doc();
9346
- if (!parsedInput.productId) {
9347
- parsedInput.productId = docRef.id;
9348
- }
9349
- await firestoreRetry(() => docRef.set({
9350
- status: "queued",
9351
- payload: parsedInput,
9352
- createdAt: admin.firestore.FieldValue.serverTimestamp(),
9353
- uid: effectiveUid || null
9354
- }), { operationName: "premortem_queue_set" });
9355
- return { content: [{ type: "text", text: JSON.stringify({ jobId: docRef.id, productId: parsedInput.productId }) }] };
8955
+ const { id: queuedId } = await createPremortem({ status: "queued", payload: parsedInput });
8956
+ if (!parsedInput.productId)
8957
+ parsedInput.productId = queuedId;
8958
+ return { content: [{ type: "text", text: JSON.stringify({ jobId: queuedId, productId: parsedInput.productId }) }] };
9356
8959
  }
9357
8960
  case "premortem_status": {
9358
8961
  const { jobId } = args;
9359
- const app = getApps4().length > 0 ? getApp4() : void 0;
9360
- if (!app)
9361
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9362
- const db2 = getFirestore4(app);
9363
- const snap = await db2.collection("premortem_jobs").doc(jobId).get();
9364
- if (!snap.exists)
8962
+ const data = await getPremortem(jobId);
8963
+ if (!data)
9365
8964
  throw new McpError(ErrorCode.InvalidRequest, "Job not found");
9366
- const data = snap.data() || {};
9367
8965
  return {
9368
8966
  content: [{
9369
8967
  type: "text",
@@ -9379,47 +8977,36 @@ Competitive threats: ${competitors}` : ""
9379
8977
  }
9380
8978
  case "premortem_kick": {
9381
8979
  const { jobId } = args;
9382
- const app = getApps4().length > 0 ? getApp4() : void 0;
9383
- if (!app)
9384
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9385
- const db2 = getFirestore4(app);
9386
- const ref = db2.collection("premortem_jobs").doc(jobId);
9387
- const snap = await ref.get();
9388
- if (!snap.exists)
8980
+ const data = await getPremortem(jobId);
8981
+ if (!data)
9389
8982
  throw new McpError(ErrorCode.InvalidRequest, "Job not found");
9390
- const data = snap.data() || {};
9391
8983
  if (data.status === "completed") {
9392
8984
  return { content: [{ type: "text", text: JSON.stringify({ kicked: false, status: "completed" }) }] };
9393
8985
  }
9394
8986
  const kickInput = data.payload;
9395
- if (!kickInput.productId) {
9396
- kickInput.productId = ref.id;
9397
- }
9398
- await firestoreRetry(() => ref.update({ status: "running", payload: kickInput, startedAt: admin.firestore.FieldValue.serverTimestamp() }), { operationName: "premortem_kick_start" });
8987
+ if (!kickInput.productId)
8988
+ kickInput.productId = jobId;
8989
+ await updatePremortem(jobId, { status: "running", payload: kickInput });
9399
8990
  const update = async (patch) => {
9400
- await firestoreRetry(() => ref.update({ ...patch, updatedAt: admin.firestore.FieldValue.serverTimestamp() }), { operationName: "premortem_kick_progress" });
8991
+ await updatePremortem(jobId, patch);
9401
8992
  };
9402
8993
  const result = await Promise.race([
9403
8994
  runRolesAndSummarizeIncremental(kickInput, update),
9404
8995
  new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 10 * 60 * 1e3))
9405
8996
  ]);
9406
8997
  const doc = PMJsonSchema.parse(result);
9407
- await firestoreRetry(() => ref.update({ status: "completed", result: doc, progress: 1, finishedAt: admin.firestore.FieldValue.serverTimestamp() }), { operationName: "premortem_kick_complete" });
8998
+ await updatePremortem(jobId, { status: "completed", result: doc, progress: 1 });
9408
8999
  return { content: [{ type: "text", text: JSON.stringify({ kicked: true, status: "completed", productId: kickInput.productId }) }] };
9409
9000
  }
9410
9001
  case "premortem_list": {
9411
9002
  const { limit = 10 } = args;
9412
- const app = getApps4().length > 0 ? getApp4() : void 0;
9413
- if (!app)
9414
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9415
- const db2 = getFirestore4(app);
9416
- const snapshot = await db2.collection("premortem_jobs").where("uid", "==", effectiveUid).orderBy("createdAt", "desc").limit(limit).get();
9417
- const premortems = snapshot.docs.map((doc) => ({
9418
- id: doc.id,
9419
- product_id: doc.data().payload?.productId || doc.id,
9420
- status: doc.data().status,
9421
- productName: doc.data().payload?.project?.name || doc.data().result?.project?.name || "Untitled",
9422
- createdAt: doc.data().createdAt?.toDate()?.toISOString()
9003
+ const docs = await listPremortems({ limit });
9004
+ const premortems = docs.map((d) => ({
9005
+ id: d.id,
9006
+ product_id: d.payload?.productId || d.id,
9007
+ status: d.status,
9008
+ productName: d.payload?.project?.name || d.result?.project?.name || "Untitled",
9009
+ createdAt: d.createdAt?._seconds ? new Date(d.createdAt._seconds * 1e3).toISOString() : d.createdAt
9423
9010
  }));
9424
9011
  return { content: [{ type: "text", text: JSON.stringify(premortems, null, 2) }] };
9425
9012
  }
@@ -9545,15 +9132,10 @@ Meta: ${JSON.stringify(output.meta)}` }
9545
9132
  if (!premortem_id || !product_id) {
9546
9133
  throw new McpError(ErrorCode.InvalidParams, "premortem_id and product_id are required");
9547
9134
  }
9548
- const app = getApps4().length > 0 ? getApp4() : void 0;
9549
- if (!app)
9550
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
9551
- const db2 = getFirestore4(app);
9552
- const snap = await db2.collection("premortem_jobs").doc(premortem_id).get();
9553
- if (!snap.exists) {
9135
+ const data = await getPremortem(premortem_id);
9136
+ if (!data) {
9554
9137
  throw new McpError(ErrorCode.InvalidRequest, "Premortem not found");
9555
9138
  }
9556
- const data = snap.data();
9557
9139
  if (data?.status !== "completed" || !data?.result) {
9558
9140
  throw new McpError(ErrorCode.InvalidRequest, "Premortem not completed yet");
9559
9141
  }
@@ -10035,7 +9617,6 @@ Meta: ${JSON.stringify(output.meta)}` }
10035
9617
  if (!product_id || !entityName || !entity_type || !entityDesc) {
10036
9618
  throw new McpError(ErrorCode.InvalidParams, "product_id, name, entity_type, and description are required");
10037
9619
  }
10038
- initFirebase();
10039
9620
  const slug = entityName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 40);
10040
9621
  const entityId = `${entity_type}:${slug}`;
10041
9622
  const now = /* @__PURE__ */ new Date();
@@ -10185,24 +9766,15 @@ Meta: ${JSON.stringify(output.meta)}` }
10185
9766
  // ─────────────────────────────────────────────────────────────────
10186
9767
  case "template_list": {
10187
9768
  const { type, deep_dive_id } = args;
10188
- initFirebase();
10189
- const app = getApps4().length > 0 ? getApp4() : void 0;
10190
- if (!app)
10191
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
10192
- const db2 = getFirestore4(app);
10193
- const snapshot = await db2.collection("templates").where("uid", "==", effectiveUid).limit(100).get();
10194
- let templates = snapshot.docs.map((doc) => ({
10195
- id: doc.id,
10196
- type: doc.data().type,
10197
- status: doc.data().status,
10198
- sourceDeepDiveId: doc.data().sourceDeepDiveId,
10199
- currentAct: doc.data().discoverySession?.currentAct || 1,
10200
- createdAt: doc.data().createdAt?.toDate?.()?.toISOString() || null,
10201
- updatedAt: doc.data().updatedAt?.toDate?.()?.toISOString() || null
9769
+ let templates = (await listTemplates({ type })).map((d) => ({
9770
+ id: d.id,
9771
+ type: d.type,
9772
+ status: d.status,
9773
+ sourceDeepDiveId: d.sourceDeepDiveId,
9774
+ currentAct: d.discoverySession?.currentAct || 1,
9775
+ createdAt: d.createdAt?._seconds ? new Date(d.createdAt._seconds * 1e3).toISOString() : d.createdAt || null,
9776
+ updatedAt: d.updatedAt?._seconds ? new Date(d.updatedAt._seconds * 1e3).toISOString() : d.updatedAt || null
10202
9777
  }));
10203
- if (type) {
10204
- templates = templates.filter((t) => t.type === type);
10205
- }
10206
9778
  if (deep_dive_id) {
10207
9779
  templates = templates.filter((t) => t.sourceDeepDiveId === deep_dive_id);
10208
9780
  }
@@ -10217,31 +9789,21 @@ Meta: ${JSON.stringify(output.meta)}` }
10217
9789
  const { template_id } = args;
10218
9790
  if (!template_id)
10219
9791
  throw new McpError(ErrorCode.InvalidParams, "template_id is required");
10220
- initFirebase();
10221
- const app = getApps4().length > 0 ? getApp4() : void 0;
10222
- if (!app)
10223
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
10224
- const db2 = getFirestore4(app);
10225
- const doc = await db2.collection("templates").doc(template_id).get();
10226
- if (!doc.exists) {
9792
+ const data = await getTemplate(template_id);
9793
+ if (!data)
10227
9794
  throw new McpError(ErrorCode.InvalidRequest, "Template not found");
10228
- }
10229
- const data = doc.data();
10230
- if (data?.uid !== effectiveUid) {
10231
- throw new McpError(ErrorCode.InvalidRequest, "Not authorized to access this template");
10232
- }
10233
9795
  return {
10234
9796
  content: [{
10235
9797
  type: "text",
10236
9798
  text: JSON.stringify({
10237
- id: doc.id,
9799
+ id: data.id,
10238
9800
  type: data.type,
10239
9801
  status: data.status,
10240
9802
  sourceDeepDiveId: data.sourceDeepDiveId,
10241
9803
  discoverySession: data.discoverySession,
10242
9804
  output: data.output,
10243
- createdAt: data.createdAt?.toDate?.()?.toISOString() || null,
10244
- updatedAt: data.updatedAt?.toDate?.()?.toISOString() || null
9805
+ createdAt: data.createdAt?._seconds ? new Date(data.createdAt._seconds * 1e3).toISOString() : data.createdAt || null,
9806
+ updatedAt: data.updatedAt?._seconds ? new Date(data.updatedAt._seconds * 1e3).toISOString() : data.updatedAt || null
10245
9807
  })
10246
9808
  }]
10247
9809
  };
@@ -10252,18 +9814,10 @@ Meta: ${JSON.stringify(output.meta)}` }
10252
9814
  throw new McpError(ErrorCode.InvalidParams, "type is required");
10253
9815
  if (!deep_dive_id)
10254
9816
  throw new McpError(ErrorCode.InvalidParams, "deep_dive_id is required");
10255
- initFirebase();
10256
- const app = getApps4().length > 0 ? getApp4() : void 0;
10257
- if (!app)
10258
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
10259
- const db2 = getFirestore4(app);
10260
- const deepDiveDoc = await db2.collection("premortem_jobs").doc(deep_dive_id).get();
10261
- if (!deepDiveDoc.exists) {
9817
+ const deepDiveData = await getPremortem(deep_dive_id);
9818
+ if (!deepDiveData) {
10262
9819
  throw new McpError(ErrorCode.InvalidRequest, "Deep Dive not found");
10263
9820
  }
10264
- if (deepDiveDoc.data()?.uid !== effectiveUid) {
10265
- throw new McpError(ErrorCode.InvalidRequest, "Not authorized to access this Deep Dive");
10266
- }
10267
9821
  const templateId = `tpl_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
10268
9822
  const now = /* @__PURE__ */ new Date();
10269
9823
  let constraintThresholds = [];
@@ -10278,8 +9832,7 @@ Meta: ${JSON.stringify(output.meta)}` }
10278
9832
  category: c.category
10279
9833
  }));
10280
9834
  }
10281
- await firestoreRetry(() => db2.collection("templates").doc(templateId).set({
10282
- uid: effectiveUid,
9835
+ await createTemplate({
10283
9836
  type,
10284
9837
  status: "discovery_active",
10285
9838
  sourceDeepDiveId: deep_dive_id,
@@ -10290,10 +9843,8 @@ Meta: ${JSON.stringify(output.meta)}` }
10290
9843
  ...rgr_mode ? { rgr_mode: true } : {},
10291
9844
  ...constraintThresholds.length > 0 ? { constraintThresholds } : {}
10292
9845
  }
10293
- },
10294
- createdAt: now,
10295
- updatedAt: now
10296
- }), { operationName: "template_create" });
9846
+ }
9847
+ }, templateId);
10297
9848
  return {
10298
9849
  content: [{
10299
9850
  type: "text",
@@ -10311,19 +9862,10 @@ Meta: ${JSON.stringify(output.meta)}` }
10311
9862
  throw new McpError(ErrorCode.InvalidParams, "template_id is required");
10312
9863
  if (!message)
10313
9864
  throw new McpError(ErrorCode.InvalidParams, "message is required");
10314
- initFirebase();
10315
- const app = getApps4().length > 0 ? getApp4() : void 0;
10316
- if (!app)
10317
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
10318
- const db2 = getFirestore4(app);
10319
- const templateDoc = await db2.collection("templates").doc(template_id).get();
10320
- if (!templateDoc.exists) {
9865
+ const templateData = await getTemplate(template_id);
9866
+ if (!templateData) {
10321
9867
  throw new McpError(ErrorCode.InvalidRequest, "Template not found");
10322
9868
  }
10323
- const templateData = templateDoc.data();
10324
- if (templateData?.uid !== effectiveUid) {
10325
- throw new McpError(ErrorCode.InvalidRequest, "Not authorized to access this template");
10326
- }
10327
9869
  if (templateData.status === "completed") {
10328
9870
  return {
10329
9871
  content: [{
@@ -10338,9 +9880,9 @@ Meta: ${JSON.stringify(output.meta)}` }
10338
9880
  }
10339
9881
  let deepDiveData = null;
10340
9882
  if (templateData.sourceDeepDiveId) {
10341
- const ddDoc = await db2.collection("premortem_jobs").doc(templateData.sourceDeepDiveId).get();
10342
- if (ddDoc.exists && ddDoc.data()?.result) {
10343
- deepDiveData = ddDoc.data()?.result;
9883
+ const ddData = await getPremortem(templateData.sourceDeepDiveId);
9884
+ if (ddData?.result) {
9885
+ deepDiveData = ddData.result;
10344
9886
  }
10345
9887
  }
10346
9888
  const response = await generateTemplateResponse(message, {
@@ -10382,16 +9924,15 @@ Meta: ${JSON.stringify(output.meta)}` }
10382
9924
  }
10383
9925
  });
10384
9926
  }
10385
- await firestoreRetry(() => db2.collection("templates").doc(template_id).update({
9927
+ await updateTemplate(template_id, {
10386
9928
  status: response.completed ? "completed" : "discovery_active",
10387
9929
  discoverySession: {
10388
9930
  currentAct: response.suggestedAct || templateData.discoverySession?.currentAct || 1,
10389
9931
  messages: [...templateData.discoverySession?.messages || [], userMsg, assistantMsg],
10390
9932
  extractedContext: updatedContext
10391
9933
  },
10392
- output: response.output || null,
10393
- updatedAt: /* @__PURE__ */ new Date()
10394
- }), { operationName: "template_update" });
9934
+ output: response.output || null
9935
+ });
10395
9936
  if (response.completed && templateData.type === "testing" && response.output?.testCases?.length > 0) {
10396
9937
  const graphProductId = updatedContext.product_id || templateData.sourceDeepDiveId;
10397
9938
  if (graphProductId) {
@@ -10834,7 +10375,6 @@ Raw counts: ${JSON.stringify({
10834
10375
  if (!product_id) {
10835
10376
  throw new McpError(ErrorCode.InvalidParams, "product_id is required");
10836
10377
  }
10837
- initFirebase();
10838
10378
  const [entities, edges, constraintNodes, bindings, existingMeta] = await Promise.all([
10839
10379
  getAllEntities(product_id),
10840
10380
  getAllEdges(product_id),
@@ -10865,11 +10405,7 @@ Raw counts: ${JSON.stringify({
10865
10405
  });
10866
10406
  await recordScoreSnapshot(product_id, metrics, "metrics_refresh");
10867
10407
  try {
10868
- const metricsApp = getApps4().length > 0 ? getApp4() : void 0;
10869
- if (metricsApp) {
10870
- const metricsDb = getFirestore4(metricsApp);
10871
- await firestoreRetry(() => metricsDb.collection("premortem_jobs").doc(product_id).update({ metrics }), { operationName: "graph_metrics_sync" });
10872
- }
10408
+ await updatePremortem(product_id, { metrics });
10873
10409
  } catch (e) {
10874
10410
  console.error("Metrics sync to premortem_jobs failed (non-fatal):", e);
10875
10411
  }
@@ -10890,7 +10426,6 @@ ${JSON.stringify(metrics, null, 2)}` }
10890
10426
  if (!product_id || !file_path) {
10891
10427
  throw new McpError(ErrorCode.InvalidParams, "product_id and file_path are required");
10892
10428
  }
10893
- initFirebase();
10894
10429
  const [rgrEntities, rgrEdges, rgrConstraints, rgrBindings] = await Promise.all([
10895
10430
  getAllEntities(product_id),
10896
10431
  getAllEdges(product_id),
@@ -10959,7 +10494,6 @@ ${JSON.stringify(metrics, null, 2)}` }
10959
10494
  if (!validPhases.includes(phase)) {
10960
10495
  throw new McpError(ErrorCode.InvalidParams, `Invalid phase "${phase}". Must be one of: ${validPhases.join(", ")}`);
10961
10496
  }
10962
- initFirebase();
10963
10497
  const meta = await getGraphMetadata(product_id);
10964
10498
  const existing = meta?.rgr_completed_phases ?? [];
10965
10499
  if (existing.includes(phase)) {
@@ -11021,7 +10555,6 @@ ${JSON.stringify(metrics, null, 2)}` }
11021
10555
  if (!product_id) {
11022
10556
  throw new McpError(ErrorCode.InvalidParams, "product_id is required");
11023
10557
  }
11024
- initFirebase();
11025
10558
  const [genEntities, genEdges, genConstraints, genBindings] = await Promise.all([
11026
10559
  getAllEntities(product_id),
11027
10560
  getAllEdges(product_id),
@@ -11215,9 +10748,11 @@ Meta: ${JSON.stringify({
11215
10748
  if (!product_id) {
11216
10749
  throw new McpError(ErrorCode.InvalidParams, "product_id is required");
11217
10750
  }
11218
- const { report, reportId } = await generateReadinessReport({
10751
+ const { report, reportId } = await generateReadinessReportViaProxy({
11219
10752
  productId: product_id,
11220
- ownerUid: effectiveUid
10753
+ ownerUid: effectiveUid,
10754
+ computeMetricsFromGraph,
10755
+ computeGrade
11221
10756
  });
11222
10757
  const reportMd = buildReportMarkdown(report, reportId);
11223
10758
  const snippets = buildBadgeSnippets(report, reportId);
@@ -11286,15 +10821,8 @@ Meta: ${JSON.stringify({
11286
10821
  addEdges: (pid, edges) => addEdges(pid, edges),
11287
10822
  upsertBindings: (pid, bindings) => upsertBindings(pid, bindings),
11288
10823
  createDeepDiveJob: async (payload) => {
11289
- initFirebase();
11290
- const jobApp = getApps4().length > 0 ? getApp4() : void 0;
11291
- if (!jobApp)
11292
- throw new McpError(ErrorCode.InternalError, "Firebase not initialized");
11293
- const jobDb = getFirestore4(jobApp);
11294
- const docRef = jobDb.collection("premortem_jobs").doc();
11295
10824
  const runInput = {
11296
10825
  mode: "product",
11297
- productId: docRef.id,
11298
10826
  project: {
11299
10827
  name: payload.extracted.productName,
11300
10828
  brief: payload.extracted.productBrief,
@@ -11317,16 +10845,16 @@ Meta: ${JSON.stringify({
11317
10845
  artifacts.target_users = payload.extracted.targetUsers;
11318
10846
  if (Object.keys(artifacts).length > 0)
11319
10847
  runInput.conversational_artifacts = artifacts;
11320
- await firestoreRetry(() => docRef.set({
10848
+ const { id: newJobId } = await createPremortem({
11321
10849
  status: "queued",
11322
- payload: runInput,
11323
- createdAt: admin.firestore.FieldValue.serverTimestamp(),
11324
- uid: effectiveUid || null,
10850
+ payload: { ...runInput, productId: void 0 },
11325
10851
  source: "code_audit",
11326
10852
  mode: "product",
11327
10853
  codeContext: payload.codeContext || null
11328
- }), { operationName: "code_audit_create_job" });
11329
- return { jobId: docRef.id };
10854
+ });
10855
+ runInput.productId = newJobId;
10856
+ await updatePremortem(newJobId, { payload: runInput });
10857
+ return { jobId: newJobId };
11330
10858
  }
11331
10859
  });
11332
10860
  const sections = [
@@ -11450,11 +10978,7 @@ These constraints will now fire via \`constraints_auto\` when you edit matched f
11450
10978
  metrics: auditMetrics
11451
10979
  });
11452
10980
  await recordScoreSnapshot(auditArgs.product_id, auditMetrics, "code_audit");
11453
- const auditApp = getApps4().length > 0 ? getApp4() : void 0;
11454
- if (auditApp) {
11455
- const auditDb = getFirestore4(auditApp);
11456
- await firestoreRetry(() => auditDb.collection("premortem_jobs").doc(auditArgs.product_id).update({ metrics: auditMetrics }), { operationName: "code_audit_sync_metrics" });
11457
- }
10981
+ await updatePremortem(auditArgs.product_id, { metrics: auditMetrics });
11458
10982
  } catch (e) {
11459
10983
  console.error("Metrics recompute after code audit failed (non-fatal):", e);
11460
10984
  }