openplanr 1.13.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +46 -4
  2. package/dist/cli/commands/artifact.d.ts.map +1 -1
  3. package/dist/cli/commands/artifact.js +17 -6
  4. package/dist/cli/commands/artifact.js.map +1 -1
  5. package/dist/cli/commands/operate.d.ts +3 -0
  6. package/dist/cli/commands/operate.d.ts.map +1 -0
  7. package/dist/cli/commands/operate.js +568 -0
  8. package/dist/cli/commands/operate.js.map +1 -0
  9. package/dist/cli/index.js +42 -2
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/services/ai-service.d.ts +9 -2
  12. package/dist/services/ai-service.d.ts.map +1 -1
  13. package/dist/services/ai-service.js +14 -6
  14. package/dist/services/ai-service.js.map +1 -1
  15. package/dist/services/artifact-pipeline-service.d.ts +1 -0
  16. package/dist/services/artifact-pipeline-service.d.ts.map +1 -1
  17. package/dist/services/artifact-pipeline-service.js.map +1 -1
  18. package/dist/services/delta-apply-service.js +1 -1
  19. package/dist/services/delta-apply-service.js.map +1 -1
  20. package/dist/services/operate/advisors.d.ts +219 -0
  21. package/dist/services/operate/advisors.d.ts.map +1 -0
  22. package/dist/services/operate/advisors.js +745 -0
  23. package/dist/services/operate/advisors.js.map +1 -0
  24. package/dist/services/operate/artifact-route-generation.d.ts +111 -0
  25. package/dist/services/operate/artifact-route-generation.d.ts.map +1 -0
  26. package/dist/services/operate/artifact-route-generation.js +459 -0
  27. package/dist/services/operate/artifact-route-generation.js.map +1 -0
  28. package/dist/services/operate/artifacts.d.ts +26 -0
  29. package/dist/services/operate/artifacts.d.ts.map +1 -0
  30. package/dist/services/operate/artifacts.js +92 -0
  31. package/dist/services/operate/artifacts.js.map +1 -0
  32. package/dist/services/operate/canonical.d.ts +18 -0
  33. package/dist/services/operate/canonical.d.ts.map +1 -0
  34. package/dist/services/operate/canonical.js +157 -0
  35. package/dist/services/operate/canonical.js.map +1 -0
  36. package/dist/services/operate/config.d.ts +56 -0
  37. package/dist/services/operate/config.d.ts.map +1 -0
  38. package/dist/services/operate/config.js +644 -0
  39. package/dist/services/operate/config.js.map +1 -0
  40. package/dist/services/operate/consolidation.d.ts +18 -0
  41. package/dist/services/operate/consolidation.d.ts.map +1 -0
  42. package/dist/services/operate/consolidation.js +539 -0
  43. package/dist/services/operate/consolidation.js.map +1 -0
  44. package/dist/services/operate/doctor.d.ts +12 -0
  45. package/dist/services/operate/doctor.d.ts.map +1 -0
  46. package/dist/services/operate/doctor.js +307 -0
  47. package/dist/services/operate/doctor.js.map +1 -0
  48. package/dist/services/operate/engine.d.ts +53 -0
  49. package/dist/services/operate/engine.d.ts.map +1 -0
  50. package/dist/services/operate/engine.js +1097 -0
  51. package/dist/services/operate/engine.js.map +1 -0
  52. package/dist/services/operate/event-store.d.ts +57 -0
  53. package/dist/services/operate/event-store.d.ts.map +1 -0
  54. package/dist/services/operate/event-store.js +232 -0
  55. package/dist/services/operate/event-store.js.map +1 -0
  56. package/dist/services/operate/evidence-cache.d.ts +17 -0
  57. package/dist/services/operate/evidence-cache.d.ts.map +1 -0
  58. package/dist/services/operate/evidence-cache.js +96 -0
  59. package/dist/services/operate/evidence-cache.js.map +1 -0
  60. package/dist/services/operate/evidence-import.d.ts +40 -0
  61. package/dist/services/operate/evidence-import.d.ts.map +1 -0
  62. package/dist/services/operate/evidence-import.js +340 -0
  63. package/dist/services/operate/evidence-import.js.map +1 -0
  64. package/dist/services/operate/evidence-readiness.d.ts +8 -0
  65. package/dist/services/operate/evidence-readiness.d.ts.map +1 -0
  66. package/dist/services/operate/evidence-readiness.js +78 -0
  67. package/dist/services/operate/evidence-readiness.js.map +1 -0
  68. package/dist/services/operate/evidence.d.ts +71 -0
  69. package/dist/services/operate/evidence.d.ts.map +1 -0
  70. package/dist/services/operate/evidence.js +1022 -0
  71. package/dist/services/operate/evidence.js.map +1 -0
  72. package/dist/services/operate/ids.d.ts +7 -0
  73. package/dist/services/operate/ids.d.ts.map +1 -0
  74. package/dist/services/operate/ids.js +23 -0
  75. package/dist/services/operate/ids.js.map +1 -0
  76. package/dist/services/operate/index.d.ts +14 -0
  77. package/dist/services/operate/index.d.ts.map +1 -0
  78. package/dist/services/operate/index.js +881 -0
  79. package/dist/services/operate/index.js.map +1 -0
  80. package/dist/services/operate/journal.d.ts +34 -0
  81. package/dist/services/operate/journal.d.ts.map +1 -0
  82. package/dist/services/operate/journal.js +229 -0
  83. package/dist/services/operate/journal.js.map +1 -0
  84. package/dist/services/operate/legacy-import-service.d.ts +58 -0
  85. package/dist/services/operate/legacy-import-service.d.ts.map +1 -0
  86. package/dist/services/operate/legacy-import-service.js +922 -0
  87. package/dist/services/operate/legacy-import-service.js.map +1 -0
  88. package/dist/services/operate/lifecycle.d.ts +88 -0
  89. package/dist/services/operate/lifecycle.d.ts.map +1 -0
  90. package/dist/services/operate/lifecycle.js +556 -0
  91. package/dist/services/operate/lifecycle.js.map +1 -0
  92. package/dist/services/operate/lock-service.d.ts +37 -0
  93. package/dist/services/operate/lock-service.d.ts.map +1 -0
  94. package/dist/services/operate/lock-service.js +208 -0
  95. package/dist/services/operate/lock-service.js.map +1 -0
  96. package/dist/services/operate/maintenance.d.ts +38 -0
  97. package/dist/services/operate/maintenance.d.ts.map +1 -0
  98. package/dist/services/operate/maintenance.js +699 -0
  99. package/dist/services/operate/maintenance.js.map +1 -0
  100. package/dist/services/operate/outcomes.d.ts +48 -0
  101. package/dist/services/operate/outcomes.d.ts.map +1 -0
  102. package/dist/services/operate/outcomes.js +312 -0
  103. package/dist/services/operate/outcomes.js.map +1 -0
  104. package/dist/services/operate/pipeline-handoff.d.ts +138 -0
  105. package/dist/services/operate/pipeline-handoff.d.ts.map +1 -0
  106. package/dist/services/operate/pipeline-handoff.js +175 -0
  107. package/dist/services/operate/pipeline-handoff.js.map +1 -0
  108. package/dist/services/operate/projection-persistence.d.ts +55 -0
  109. package/dist/services/operate/projection-persistence.d.ts.map +1 -0
  110. package/dist/services/operate/projection-persistence.js +393 -0
  111. package/dist/services/operate/projection-persistence.js.map +1 -0
  112. package/dist/services/operate/projection.d.ts +4 -0
  113. package/dist/services/operate/projection.d.ts.map +1 -0
  114. package/dist/services/operate/projection.js +128 -0
  115. package/dist/services/operate/projection.js.map +1 -0
  116. package/dist/services/operate/protocol.d.ts +60 -0
  117. package/dist/services/operate/protocol.d.ts.map +1 -0
  118. package/dist/services/operate/protocol.js +62 -0
  119. package/dist/services/operate/protocol.js.map +1 -0
  120. package/dist/services/operate/read-only-providers.d.ts +38 -0
  121. package/dist/services/operate/read-only-providers.d.ts.map +1 -0
  122. package/dist/services/operate/read-only-providers.js +286 -0
  123. package/dist/services/operate/read-only-providers.js.map +1 -0
  124. package/dist/services/operate/redaction.d.ts +47 -0
  125. package/dist/services/operate/redaction.d.ts.map +1 -0
  126. package/dist/services/operate/redaction.js +279 -0
  127. package/dist/services/operate/redaction.js.map +1 -0
  128. package/dist/services/operate/reducer.d.ts +10 -0
  129. package/dist/services/operate/reducer.d.ts.map +1 -0
  130. package/dist/services/operate/reducer.js +11 -0
  131. package/dist/services/operate/reducer.js.map +1 -0
  132. package/dist/services/operate/routes.d.ts +49 -0
  133. package/dist/services/operate/routes.d.ts.map +1 -0
  134. package/dist/services/operate/routes.js +996 -0
  135. package/dist/services/operate/routes.js.map +1 -0
  136. package/dist/services/operate/shipment-observer.d.ts +14 -0
  137. package/dist/services/operate/shipment-observer.d.ts.map +1 -0
  138. package/dist/services/operate/shipment-observer.js +321 -0
  139. package/dist/services/operate/shipment-observer.js.map +1 -0
  140. package/dist/services/operate/stalled-item-service.d.ts +18 -0
  141. package/dist/services/operate/stalled-item-service.d.ts.map +1 -0
  142. package/dist/services/operate/stalled-item-service.js +121 -0
  143. package/dist/services/operate/stalled-item-service.js.map +1 -0
  144. package/dist/services/operate/subprocess-env.d.ts +4 -0
  145. package/dist/services/operate/subprocess-env.d.ts.map +1 -0
  146. package/dist/services/operate/subprocess-env.js +45 -0
  147. package/dist/services/operate/subprocess-env.js.map +1 -0
  148. package/dist/services/operate/types.d.ts +729 -0
  149. package/dist/services/operate/types.d.ts.map +1 -0
  150. package/dist/services/operate/types.js +13 -0
  151. package/dist/services/operate/types.js.map +1 -0
  152. package/dist/services/operate/workspace.d.ts +59 -0
  153. package/dist/services/operate/workspace.d.ts.map +1 -0
  154. package/dist/services/operate/workspace.js +316 -0
  155. package/dist/services/operate/workspace.js.map +1 -0
  156. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  157. package/dist/services/runtime-manager-service.js +71 -2
  158. package/dist/services/runtime-manager-service.js.map +1 -1
  159. package/package.json +10 -4
@@ -0,0 +1,1022 @@
1
+ import { lstat, mkdir, readFile, realpath, rename, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../config-service.js';
4
+ import { resolveApiKey } from '../credentials-service.js';
5
+ import { canonicalDigest, canonicalize } from './canonical.js';
6
+ import { readImportedEvidenceFile } from './evidence-import.js';
7
+ import { assertOperatingArtifact } from './protocol.js';
8
+ import { executeGitHubReadOnly, executeGitReadOnly, executeLinearReadOnlyQuery, executeRestReadOnlyJson, } from './read-only-providers.js';
9
+ import { compareSensitivity, sanitizeEvidenceItem } from './redaction.js';
10
+ import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
11
+ import { isPathInside, resolveOperatingPaths } from './workspace.js';
12
+ const TEXT_EXTENSIONS = new Set([
13
+ '.c',
14
+ '.cc',
15
+ '.css',
16
+ '.csv',
17
+ '.go',
18
+ '.graphql',
19
+ '.h',
20
+ '.html',
21
+ '.java',
22
+ '.js',
23
+ '.json',
24
+ '.jsx',
25
+ '.kt',
26
+ '.md',
27
+ '.mjs',
28
+ '.py',
29
+ '.rb',
30
+ '.rs',
31
+ '.sh',
32
+ '.sql',
33
+ '.swift',
34
+ '.toml',
35
+ '.ts',
36
+ '.tsx',
37
+ '.yaml',
38
+ '.yml',
39
+ ]);
40
+ function evidenceId(source, identity) {
41
+ return `EVD-${source}-${canonicalDigest(identity).slice('sha256:'.length, 19)}`;
42
+ }
43
+ export function evidenceFingerprintItems(items) {
44
+ return items
45
+ .map(({ id, digest, sensitivity }) => ({ id, digest, sensitivity }))
46
+ .sort((left, right) => left.id.localeCompare(right.id) ||
47
+ left.digest.localeCompare(right.digest) ||
48
+ left.sensitivity.localeCompare(right.sensitivity));
49
+ }
50
+ function ageFreshness(collectedAt, now, ttlHours = 168) {
51
+ return now.getTime() - Date.parse(collectedAt) <= ttlHours * 60 * 60 * 1_000 ? 'fresh' : 'stale';
52
+ }
53
+ async function readRoots(projectRoot, localRoot) {
54
+ const candidates = [projectRoot, await realpath(projectRoot).catch(() => projectRoot)];
55
+ for (const candidate of [...new Set(candidates)]) {
56
+ try {
57
+ return JSON.parse(await readFile(resolveOperatingPaths(candidate, { localRoot }).roots, 'utf8'));
58
+ }
59
+ catch {
60
+ // Invocation paths can differ from their canonical real path on systems
61
+ // such as macOS where /var resolves through /private/var.
62
+ }
63
+ }
64
+ throw new OperateError('E_OPERATE_NOT_INITIALIZED', 'Machine-local workspace roots are missing; run `planr operate init` again.');
65
+ }
66
+ function descriptorById(workspace) {
67
+ return new Map([workspace.controlRepository, ...workspace.components].map((entry) => [
68
+ entry.componentId,
69
+ entry,
70
+ ]));
71
+ }
72
+ async function repositoryItems(roots, workspace, budgets, now, options) {
73
+ const descriptors = descriptorById(workspace);
74
+ const results = [];
75
+ let files = 0;
76
+ let bytes = 0;
77
+ let truncated = false;
78
+ for (const [componentId, root] of Object.entries(roots.roots).sort()) {
79
+ if (Date.now() >= options.deadline) {
80
+ truncated = true;
81
+ break;
82
+ }
83
+ const descriptor = descriptors.get(componentId);
84
+ if (!descriptor)
85
+ continue;
86
+ const canonicalRoot = await realpath(root);
87
+ const names = (await executeGitReadOnly(canonicalRoot, ['ls-files', '-z']))
88
+ .split('\0')
89
+ .filter(Boolean)
90
+ .filter((name) => TEXT_EXTENSIONS.has(path.extname(name).toLowerCase()))
91
+ .sort();
92
+ for (const relativePath of names) {
93
+ if (Date.now() >= options.deadline ||
94
+ files >= budgets.maxFiles ||
95
+ results.length >= budgets.maxItems) {
96
+ truncated = true;
97
+ break;
98
+ }
99
+ const isPlanr = relativePath.startsWith('.planr/');
100
+ if ((!options.includeRepository && !isPlanr) ||
101
+ (isPlanr && !options.includePlanr && !options.includeRepository)) {
102
+ continue;
103
+ }
104
+ const target = path.join(canonicalRoot, relativePath);
105
+ const directInfo = await lstat(target).catch(() => null);
106
+ if (!directInfo)
107
+ continue;
108
+ const resolvedTarget = await realpath(target).catch(() => null);
109
+ if (!resolvedTarget)
110
+ continue;
111
+ if (!isPathInside(canonicalRoot, resolvedTarget)) {
112
+ throw new OperateError('E_OPERATE_PATH_ESCAPE', `Tracked evidence path follows a symlink outside its repository: ${componentId}/${relativePath}.`);
113
+ }
114
+ if (directInfo.isSymbolicLink())
115
+ continue;
116
+ const info = await stat(resolvedTarget).catch(() => null);
117
+ if (!info?.isFile() || info.size > budgets.maxItemBytes)
118
+ continue;
119
+ if (bytes + info.size > budgets.maxBytes) {
120
+ truncated = true;
121
+ break;
122
+ }
123
+ const content = await readFile(resolvedTarget, 'utf8').catch(() => null);
124
+ if (content === null || content.includes('\0'))
125
+ continue;
126
+ files += 1;
127
+ bytes += Buffer.byteLength(content);
128
+ const collectedAt = now.toISOString();
129
+ const repository = {
130
+ componentId,
131
+ canonicalRemote: `https://${descriptor.canonicalRemote}`,
132
+ revision: descriptor.pinnedRevision,
133
+ configuredBranch: descriptor.configuredBranch,
134
+ dirtyFingerprint: descriptor.dirtyFingerprint,
135
+ relativePath,
136
+ digest: canonicalDigest(content),
137
+ freshness: ageFreshness(collectedAt, now),
138
+ sensitivity: 'internal',
139
+ collectedAt,
140
+ };
141
+ if (options.includeRepository && results.length < budgets.maxItems) {
142
+ results.push({
143
+ id: evidenceId('repository', `${componentId}:${relativePath}:${repository.digest}`),
144
+ source: 'repository',
145
+ location: `${componentId}/${relativePath}`,
146
+ content,
147
+ collectedAt,
148
+ observedFrom: null,
149
+ observedTo: collectedAt,
150
+ freshness: repository.freshness,
151
+ sensitivity: repository.sensitivity,
152
+ claimTypes: ['code', 'architecture'],
153
+ quality: 'observed',
154
+ coverage: 'partial',
155
+ repository,
156
+ });
157
+ }
158
+ if (isPlanr && options.includePlanr && results.length < budgets.maxItems) {
159
+ const planrClaims = [
160
+ 'planning',
161
+ ...(relativePath.includes('/stories/') || relativePath.includes('/specs/')
162
+ ? ['user-journey']
163
+ : []),
164
+ ...(relativePath.includes('outcome') || relativePath.includes('provenance')
165
+ ? ['outcome']
166
+ : []),
167
+ ...(relativePath.includes('/support') ? ['support', 'operations'] : []),
168
+ ];
169
+ results.push({
170
+ id: evidenceId('planr', `${componentId}:${relativePath}:${repository.digest}`),
171
+ source: 'planr',
172
+ location: `${componentId}/${relativePath}`,
173
+ content,
174
+ collectedAt,
175
+ observedFrom: null,
176
+ observedTo: collectedAt,
177
+ freshness: repository.freshness,
178
+ sensitivity: repository.sensitivity,
179
+ claimTypes: planrClaims,
180
+ quality: 'verified',
181
+ coverage: 'partial',
182
+ repository,
183
+ });
184
+ }
185
+ }
186
+ }
187
+ return { items: results, truncated };
188
+ }
189
+ async function gitItems(roots, workspace, now, deadline) {
190
+ const descriptors = descriptorById(workspace);
191
+ const results = [];
192
+ for (const [componentId, root] of Object.entries(roots.roots).sort()) {
193
+ const remaining = deadline - Date.now();
194
+ if (remaining <= 0)
195
+ return { items: results, truncated: true };
196
+ const descriptor = descriptors.get(componentId);
197
+ if (!descriptor)
198
+ continue;
199
+ // An unborn Git branch has no history to cite. The workspace manifest uses
200
+ // Git's null OID for that state; do not manufacture a change-history item
201
+ // that could incorrectly satisfy an advisor readiness requirement.
202
+ if (/^0{40,64}$/u.test(descriptor.pinnedRevision))
203
+ continue;
204
+ const content = await executeGitReadOnly(root, [
205
+ 'log',
206
+ '--since=30.days',
207
+ '--max-count=200',
208
+ '--date=iso-strict',
209
+ '--pretty=format:%H%x09%ad%x09%s',
210
+ ], { timeoutMs: remaining });
211
+ const collectedAt = now.toISOString();
212
+ results.push({
213
+ id: evidenceId('git', `${componentId}:${descriptor.pinnedRevision}:${content}`),
214
+ source: 'git',
215
+ location: `${componentId}/history`,
216
+ content,
217
+ collectedAt,
218
+ observedFrom: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1_000).toISOString(),
219
+ observedTo: collectedAt,
220
+ freshness: 'fresh',
221
+ sensitivity: 'internal',
222
+ claimTypes: ['change-history'],
223
+ quality: 'verified',
224
+ coverage: 'partial',
225
+ repository: {
226
+ componentId,
227
+ canonicalRemote: `https://${descriptor.canonicalRemote}`,
228
+ revision: descriptor.pinnedRevision,
229
+ configuredBranch: descriptor.configuredBranch,
230
+ dirtyFingerprint: descriptor.dirtyFingerprint,
231
+ relativePath: '.git',
232
+ digest: canonicalDigest(content),
233
+ freshness: 'fresh',
234
+ sensitivity: 'internal',
235
+ collectedAt,
236
+ },
237
+ });
238
+ }
239
+ return { items: results, truncated: false };
240
+ }
241
+ function boundedRecordsContent(key, records, maxBytes) {
242
+ const selected = [...records];
243
+ let content = canonicalize({ [key]: selected });
244
+ while (selected.length > 0 && Buffer.byteLength(content) > maxBytes) {
245
+ selected.pop();
246
+ content = canonicalize({ [key]: selected });
247
+ }
248
+ if (Buffer.byteLength(content) > maxBytes) {
249
+ throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', `${key} evidence cannot fit inside the configured item limit.`);
250
+ }
251
+ return {
252
+ content,
253
+ records: selected.length,
254
+ truncated: selected.length < records.length,
255
+ };
256
+ }
257
+ function githubIdentity(canonicalRemote) {
258
+ let url;
259
+ try {
260
+ url = new URL(canonicalRemote.includes('://') ? canonicalRemote : `https://${canonicalRemote}`);
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ if (url.protocol !== 'https:' ||
266
+ url.hostname.toLowerCase() !== 'github.com' ||
267
+ url.username ||
268
+ url.password ||
269
+ (url.port && url.port !== '443')) {
270
+ return null;
271
+ }
272
+ const segments = url.pathname
273
+ .replace(/\.git$/i, '')
274
+ .split('/')
275
+ .filter(Boolean);
276
+ if (segments.length !== 2 || segments.some((segment) => !/^[A-Za-z0-9_.-]+$/.test(segment))) {
277
+ return null;
278
+ }
279
+ return { owner: segments[0], repository: segments[1] };
280
+ }
281
+ async function githubPages(input) {
282
+ const results = [];
283
+ const pageSize = Math.min(100, Math.max(1, input.maxRecords));
284
+ const pageLimit = Math.min(5, Math.ceil(input.maxRecords / pageSize));
285
+ if (input.maxBytes < 1_024 || pageLimit < 1)
286
+ return results;
287
+ const perPageBytes = Math.max(1_024, Math.floor(input.maxBytes / pageLimit));
288
+ for (let page = 1; page <= pageLimit && results.length < input.maxRecords; page += 1) {
289
+ const remaining = input.deadline - Date.now();
290
+ if (remaining <= 0)
291
+ break;
292
+ const endpoint = new URL(input.endpoint);
293
+ endpoint.searchParams.set('per_page', String(pageSize));
294
+ endpoint.searchParams.set('page', String(page));
295
+ const response = await executeRestReadOnlyJson(endpoint, {
296
+ method: 'GET',
297
+ allowedHosts: ['api.github.com'],
298
+ timeoutMs: remaining,
299
+ maxBytes: Math.min(perPageBytes, 5 * 1024 * 1024),
300
+ fetchImpl: input.fetchImpl,
301
+ headers: {
302
+ accept: 'application/vnd.github+json',
303
+ authorization: `Bearer ${input.token}`,
304
+ 'x-github-api-version': '2022-11-28',
305
+ },
306
+ });
307
+ const pageRecords = input.select(response);
308
+ const rawPageRecords = Array.isArray(response)
309
+ ? response.length
310
+ : response &&
311
+ typeof response === 'object' &&
312
+ Array.isArray(response.check_runs)
313
+ ? response.check_runs.length
314
+ : pageRecords.length;
315
+ results.push(...pageRecords.slice(0, input.maxRecords - results.length));
316
+ if (rawPageRecords < pageSize)
317
+ break;
318
+ }
319
+ return results;
320
+ }
321
+ export async function collectGitHubEvidence(input) {
322
+ const token = input.token?.trim() ||
323
+ process.env.GH_TOKEN?.trim() ||
324
+ process.env.GITHUB_TOKEN?.trim() ||
325
+ (await executeGitHubReadOnly(input.projectRoot, ['auth', 'token', '--hostname', 'github.com'], { allowedHosts: ['github.com'], timeoutMs: input.deadline - Date.now() })).trim();
326
+ if (!token) {
327
+ throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', 'GitHub credentials are unavailable.');
328
+ }
329
+ const descriptors = [input.workspace.controlRepository, ...input.workspace.components];
330
+ const items = [];
331
+ let truncated = false;
332
+ let remainingBytes = input.budgets.maxBytes;
333
+ const maxRecords = Math.min(100, Math.max(1, Math.floor(input.budgets.maxItems / 4)));
334
+ const categoryResponseBytes = Math.max(1_024, Math.floor(input.budgets.maxBytes / 4));
335
+ for (const descriptor of descriptors) {
336
+ const identity = githubIdentity(descriptor.canonicalRemote);
337
+ if (!identity)
338
+ continue;
339
+ if (Date.now() >= input.deadline || items.length >= input.budgets.maxItems) {
340
+ truncated = true;
341
+ break;
342
+ }
343
+ const base = `https://api.github.com/repos/${encodeURIComponent(identity.owner)}/${encodeURIComponent(identity.repository)}`;
344
+ const categories = [
345
+ {
346
+ id: 'issues',
347
+ endpoint: `${base}/issues?state=all`,
348
+ claims: ['customer-signal', 'delivery'],
349
+ select: (value) => (Array.isArray(value) ? value : [])
350
+ .filter((record) => Boolean(record) && typeof record === 'object' && !('pull_request' in record))
351
+ .map((record) => ({
352
+ number: record.number ?? null,
353
+ title: record.title ?? null,
354
+ state: record.state ?? null,
355
+ createdAt: record.created_at ?? null,
356
+ updatedAt: record.updated_at ?? null,
357
+ labels: Array.isArray(record.labels)
358
+ ? record.labels
359
+ .map((label) => label && typeof label === 'object'
360
+ ? label.name
361
+ : label)
362
+ .filter((label) => typeof label === 'string')
363
+ .sort()
364
+ : [],
365
+ })),
366
+ },
367
+ {
368
+ id: 'pull-requests',
369
+ endpoint: `${base}/pulls?state=all`,
370
+ claims: ['delivery', 'change-history'],
371
+ select: (value) => (Array.isArray(value) ? value : [])
372
+ .filter((record) => Boolean(record) && typeof record === 'object')
373
+ .map((record) => ({
374
+ number: record.number ?? null,
375
+ title: record.title ?? null,
376
+ state: record.state ?? null,
377
+ draft: record.draft ?? null,
378
+ createdAt: record.created_at ?? null,
379
+ updatedAt: record.updated_at ?? null,
380
+ mergedAt: record.merged_at ?? null,
381
+ })),
382
+ },
383
+ {
384
+ id: 'releases',
385
+ endpoint: `${base}/releases`,
386
+ claims: ['delivery', 'change-history'],
387
+ select: (value) => (Array.isArray(value) ? value : [])
388
+ .filter((record) => Boolean(record) && typeof record === 'object')
389
+ .map((record) => ({
390
+ id: record.id ?? null,
391
+ tag: record.tag_name ?? null,
392
+ name: record.name ?? null,
393
+ draft: record.draft ?? null,
394
+ prerelease: record.prerelease ?? null,
395
+ createdAt: record.created_at ?? null,
396
+ publishedAt: record.published_at ?? null,
397
+ })),
398
+ },
399
+ {
400
+ id: 'checks',
401
+ endpoint: `${base}/commits/${encodeURIComponent(descriptor.pinnedRevision)}/check-runs`,
402
+ claims: ['delivery', 'reliability'],
403
+ select: (value) => {
404
+ if (!value || typeof value !== 'object')
405
+ return [];
406
+ const records = value.check_runs;
407
+ return (Array.isArray(records) ? records : [])
408
+ .filter((record) => Boolean(record) && typeof record === 'object')
409
+ .map((record) => ({
410
+ id: record.id ?? null,
411
+ name: record.name ?? null,
412
+ status: record.status ?? null,
413
+ conclusion: record.conclusion ?? null,
414
+ startedAt: record.started_at ?? null,
415
+ completedAt: record.completed_at ?? null,
416
+ }));
417
+ },
418
+ },
419
+ ];
420
+ for (const category of categories) {
421
+ if (Date.now() >= input.deadline ||
422
+ items.length >= input.budgets.maxItems ||
423
+ remainingBytes < 1_024) {
424
+ truncated = true;
425
+ break;
426
+ }
427
+ const records = (await githubPages({
428
+ token,
429
+ endpoint: category.endpoint,
430
+ deadline: input.deadline,
431
+ maxRecords,
432
+ maxBytes: Math.min(remainingBytes, categoryResponseBytes),
433
+ fetchImpl: input.fetchImpl,
434
+ select: category.select,
435
+ }));
436
+ const bounded = boundedRecordsContent(category.id, records, Math.min(input.budgets.maxItemBytes, remainingBytes));
437
+ truncated ||= bounded.truncated || records.length >= maxRecords;
438
+ remainingBytes -= Buffer.byteLength(bounded.content);
439
+ if (bounded.records === 0)
440
+ continue;
441
+ const collectedAt = input.now.toISOString();
442
+ items.push({
443
+ id: evidenceId('github', `${descriptor.componentId}:${category.id}:${bounded.content}`),
444
+ source: 'github',
445
+ location: `${descriptor.componentId}/${category.id}`,
446
+ content: bounded.content,
447
+ collectedAt,
448
+ observedFrom: null,
449
+ observedTo: collectedAt,
450
+ freshness: 'fresh',
451
+ sensitivity: 'confidential',
452
+ claimTypes: category.claims,
453
+ quality: 'observed',
454
+ coverage: bounded.truncated ? 'partial' : 'complete',
455
+ });
456
+ }
457
+ }
458
+ return { items, truncated };
459
+ }
460
+ async function linearConnectionPages(input) {
461
+ const records = [];
462
+ let after = null;
463
+ const pageSize = Math.min(100, Math.max(1, input.maxRecords));
464
+ const pageLimit = Math.min(5, Math.ceil(input.maxRecords / pageSize));
465
+ const perPageBytes = Math.max(1_024, Math.floor(input.maxBytes / Math.max(1, pageLimit)));
466
+ for (let page = 0; page < pageLimit && records.length < input.maxRecords; page += 1) {
467
+ const remaining = input.deadline - Date.now();
468
+ if (remaining <= 0)
469
+ break;
470
+ const data = await executeLinearReadOnlyQuery({
471
+ token: input.token,
472
+ query: input.query,
473
+ variables: {
474
+ ...input.variables,
475
+ first: pageSize,
476
+ after,
477
+ },
478
+ timeoutMs: remaining,
479
+ maxBytes: Math.min(perPageBytes, 5 * 1024 * 1024),
480
+ allowedHosts: ['api.linear.app'],
481
+ fetchImpl: input.fetchImpl,
482
+ });
483
+ const connection = data[input.connection];
484
+ const nodes = Array.isArray(connection?.nodes) ? connection.nodes : [];
485
+ records.push(...nodes.slice(0, input.maxRecords - records.length));
486
+ if (!connection?.pageInfo?.hasNextPage || !connection.pageInfo.endCursor)
487
+ break;
488
+ after = connection.pageInfo.endCursor;
489
+ }
490
+ return records;
491
+ }
492
+ export async function collectLinearEvidence(input) {
493
+ const token = input.token?.trim() || (await resolveApiKey('linear'))?.trim();
494
+ if (!token) {
495
+ throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', 'Linear credentials are unavailable.');
496
+ }
497
+ const config = input.teamIds ? null : await loadConfig(input.projectRoot).catch(() => null);
498
+ const teamIds = [
499
+ ...new Set((input.teamIds ?? [
500
+ config?.linear?.teamId,
501
+ ...(config?.linear?.teams ?? []).map((team) => team.id),
502
+ ]).filter((value) => Boolean(value))),
503
+ ];
504
+ const maxRecords = Math.min(100, Math.max(1, Math.floor(input.budgets.maxItems / 3)));
505
+ const connectionBudget = Math.max(1_024, Math.floor(input.budgets.maxBytes / 3));
506
+ const teams = await linearConnectionPages({
507
+ token,
508
+ query: `query OperatingTeams($first: Int!, $after: String) {
509
+ teams(first: $first, after: $after) {
510
+ nodes { id key name }
511
+ pageInfo { hasNextPage endCursor }
512
+ }
513
+ }`,
514
+ connection: 'teams',
515
+ deadline: input.deadline,
516
+ maxRecords,
517
+ maxBytes: connectionBudget,
518
+ fetchImpl: input.fetchImpl,
519
+ });
520
+ const issues = await linearConnectionPages({
521
+ token,
522
+ query: teamIds.length > 0
523
+ ? `query OperatingIssues($first: Int!, $after: String, $teamIds: [ID!]) {
524
+ issues(first: $first, after: $after, filter: { team: { id: { in: $teamIds } } }) {
525
+ nodes {
526
+ id identifier title updatedAt priority
527
+ team { id key }
528
+ state { name }
529
+ labels { nodes { name } }
530
+ }
531
+ pageInfo { hasNextPage endCursor }
532
+ }
533
+ }`
534
+ : `query OperatingIssues($first: Int!, $after: String) {
535
+ issues(first: $first, after: $after) {
536
+ nodes {
537
+ id identifier title updatedAt priority
538
+ team { id key }
539
+ state { name }
540
+ labels { nodes { name } }
541
+ }
542
+ pageInfo { hasNextPage endCursor }
543
+ }
544
+ }`,
545
+ connection: 'issues',
546
+ variables: teamIds.length > 0 ? { teamIds } : {},
547
+ deadline: input.deadline,
548
+ maxRecords,
549
+ maxBytes: connectionBudget,
550
+ fetchImpl: input.fetchImpl,
551
+ });
552
+ const projects = await linearConnectionPages({
553
+ token,
554
+ query: `query OperatingProjects($first: Int!, $after: String) {
555
+ projects(first: $first, after: $after) {
556
+ nodes {
557
+ id name updatedAt
558
+ status { name }
559
+ teams { nodes { id key name } }
560
+ }
561
+ pageInfo { hasNextPage endCursor }
562
+ }
563
+ }`,
564
+ connection: 'projects',
565
+ deadline: input.deadline,
566
+ maxRecords,
567
+ maxBytes: connectionBudget,
568
+ fetchImpl: input.fetchImpl,
569
+ });
570
+ const selectedProjects = teamIds.length === 0
571
+ ? projects
572
+ : projects.filter((project) => (project.teams?.nodes ?? []).some((team) => team.id && teamIds.includes(team.id)));
573
+ const records = [
574
+ {
575
+ id: 'teams',
576
+ values: teams.map((team) => ({
577
+ id: team.id ?? null,
578
+ key: team.key ?? null,
579
+ name: team.name ?? null,
580
+ })),
581
+ claims: ['planning', 'operations'],
582
+ },
583
+ {
584
+ id: 'issues',
585
+ values: issues.map((issue) => ({
586
+ id: issue.id ?? null,
587
+ identifier: issue.identifier ?? null,
588
+ title: issue.title ?? null,
589
+ updatedAt: issue.updatedAt ?? null,
590
+ priority: issue.priority ?? null,
591
+ team: issue.team ? { id: issue.team.id ?? null, key: issue.team.key ?? null } : null,
592
+ state: issue.state?.name ?? null,
593
+ labels: (issue.labels?.nodes ?? [])
594
+ .map((label) => label.name)
595
+ .filter((label) => Boolean(label))
596
+ .sort(),
597
+ })),
598
+ claims: ['customer-signal', 'delivery', 'planning'],
599
+ },
600
+ {
601
+ id: 'projects',
602
+ values: selectedProjects.map((project) => ({
603
+ id: project.id ?? null,
604
+ name: project.name ?? null,
605
+ updatedAt: project.updatedAt ?? null,
606
+ status: project.status?.name ?? null,
607
+ teams: (project.teams?.nodes ?? [])
608
+ .map((team) => ({
609
+ id: team.id ?? null,
610
+ key: team.key ?? null,
611
+ name: team.name ?? null,
612
+ }))
613
+ .sort((left, right) => String(left.id).localeCompare(String(right.id))),
614
+ })),
615
+ claims: ['planning', 'operations', 'delivery'],
616
+ },
617
+ ];
618
+ const items = [];
619
+ let truncated = false;
620
+ let remainingBytes = input.budgets.maxBytes;
621
+ for (const record of records) {
622
+ if (remainingBytes < 1_024 || items.length >= input.budgets.maxItems) {
623
+ truncated = true;
624
+ break;
625
+ }
626
+ const bounded = boundedRecordsContent(record.id, record.values, Math.min(input.budgets.maxItemBytes, remainingBytes));
627
+ truncated ||= bounded.truncated || record.values.length >= maxRecords;
628
+ remainingBytes -= Buffer.byteLength(bounded.content);
629
+ if (bounded.records === 0)
630
+ continue;
631
+ const collectedAt = input.now.toISOString();
632
+ items.push({
633
+ id: evidenceId('linear', `${record.id}:${bounded.content}`),
634
+ source: 'linear',
635
+ location: record.id,
636
+ content: bounded.content,
637
+ collectedAt,
638
+ observedFrom: null,
639
+ observedTo: collectedAt,
640
+ freshness: 'fresh',
641
+ sensitivity: 'confidential',
642
+ claimTypes: record.claims,
643
+ quality: 'observed',
644
+ coverage: bounded.truncated ? 'partial' : 'complete',
645
+ });
646
+ }
647
+ return { items, truncated };
648
+ }
649
+ async function fileImportItems(projectRoot, roots, budgets, now, deadline, localRoot) {
650
+ const preferencesPath = path.join(resolveOperatingPaths(projectRoot, { localRoot }).localRoot, 'preferences.json');
651
+ const preferences = JSON.parse(await readFile(preferencesPath, 'utf8'));
652
+ const importPaths = Array.isArray(preferences.importPaths)
653
+ ? preferences.importPaths.filter((entry) => typeof entry === 'string')
654
+ : [];
655
+ if (importPaths.length === 0) {
656
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'The file-import source has no configured evidence paths; rerun `planr operate init --evidence-file <path>`.');
657
+ }
658
+ const allowedRoots = Object.entries(roots.roots).map(([componentId, root]) => ({
659
+ componentId,
660
+ root,
661
+ }));
662
+ const items = [];
663
+ let bytes = 0;
664
+ let truncated = false;
665
+ for (const configuredPath of importPaths) {
666
+ if (Date.now() >= deadline ||
667
+ items.length >= budgets.maxItems ||
668
+ items.length >= budgets.maxFiles) {
669
+ truncated = true;
670
+ break;
671
+ }
672
+ const remainingBytes = budgets.maxBytes - bytes;
673
+ if (remainingBytes < 1) {
674
+ truncated = true;
675
+ break;
676
+ }
677
+ const imported = await readImportedEvidenceFile({
678
+ projectRoot,
679
+ configuredPath,
680
+ roots: allowedRoots,
681
+ maxBytes: Math.min(budgets.maxItemBytes, remainingBytes),
682
+ });
683
+ const commercialMetric = inferImportedCommercialMetric(imported.content, imported.format);
684
+ bytes += imported.byteCount;
685
+ const collectedAt = now.toISOString();
686
+ items.push({
687
+ id: evidenceId('file-import', `${imported.location}:${canonicalDigest(imported.content)}`),
688
+ source: 'file-import',
689
+ location: imported.location,
690
+ content: imported.content,
691
+ collectedAt,
692
+ observedFrom: null,
693
+ observedTo: collectedAt,
694
+ freshness: 'fresh',
695
+ sensitivity: 'internal',
696
+ claimTypes: [
697
+ 'structured-data',
698
+ imported.format,
699
+ ...(commercialMetric.commercial ? ['commercial-metric'] : []),
700
+ ],
701
+ quality: 'self-reported',
702
+ coverage: 'complete',
703
+ ...(commercialMetric.metric ? { metric: commercialMetric.metric } : {}),
704
+ });
705
+ }
706
+ return { items, truncated };
707
+ }
708
+ const COMMERCIAL_METRIC_TERMS = /\b(?:activation|arr|arpu|cac|churn|conversion|customers?|gmv|ltv|margin|mrr|price|pricing|retention|revenue|sales|spend|subscriptions?|users?)\b/i;
709
+ const NUMERIC_CLAIM = /(?:[$€£]\s*)?\b\d+(?:[.,]\d+)?\s*(?:%|percent|usd|eur|gbp|customers?|users?)?\b/i;
710
+ function validIsoWindow(from, to) {
711
+ return (typeof from === 'string' &&
712
+ typeof to === 'string' &&
713
+ Number.isFinite(Date.parse(from)) &&
714
+ Number.isFinite(Date.parse(to)) &&
715
+ Date.parse(to) >= Date.parse(from));
716
+ }
717
+ function inferImportedCommercialMetric(content, format) {
718
+ const commercial = COMMERCIAL_METRIC_TERMS.test(content) && NUMERIC_CLAIM.test(content);
719
+ if (!commercial || format !== 'json')
720
+ return { commercial };
721
+ try {
722
+ const parsed = JSON.parse(content);
723
+ const candidate = parsed.metric && typeof parsed.metric === 'object'
724
+ ? parsed.metric
725
+ : null;
726
+ if (!candidate ||
727
+ typeof candidate.identity !== 'string' ||
728
+ !candidate.identity.trim() ||
729
+ typeof candidate.query !== 'string' ||
730
+ !candidate.query.trim() ||
731
+ !validIsoWindow(candidate.observedFrom, candidate.observedTo)) {
732
+ return { commercial };
733
+ }
734
+ return {
735
+ commercial,
736
+ metric: {
737
+ identity: candidate.identity.trim(),
738
+ query: candidate.query.trim(),
739
+ observedFrom: candidate.observedFrom,
740
+ observedTo: candidate.observedTo,
741
+ },
742
+ };
743
+ }
744
+ catch {
745
+ return { commercial };
746
+ }
747
+ }
748
+ function incrementalEvidencePath(input, key) {
749
+ return path.join(resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot }).evidence, 'incremental', `${key.slice('sha256:'.length)}.json`);
750
+ }
751
+ async function readIncrementalEvidence(input, key) {
752
+ if (!input.incremental)
753
+ return null;
754
+ try {
755
+ const record = JSON.parse(await readFile(incrementalEvidencePath(input, key), 'utf8'));
756
+ if (record.implementation !== 'openplanr-operate-incremental-evidence' || record.key !== key) {
757
+ return null;
758
+ }
759
+ await assertOperatingArtifact('operating-evidence', record.evidence);
760
+ return record;
761
+ }
762
+ catch {
763
+ return null;
764
+ }
765
+ }
766
+ async function writeIncrementalEvidence(input, key, evidence) {
767
+ if (!input.persistIncremental ||
768
+ input.providers.some((provider) => ['file-import', 'github', 'linear'].includes(provider))) {
769
+ return;
770
+ }
771
+ const target = incrementalEvidencePath(input, key);
772
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
773
+ const temporary = `${target}.${process.pid}.tmp`;
774
+ const record = {
775
+ implementation: 'openplanr-operate-incremental-evidence',
776
+ key,
777
+ workspaceDigest: input.workspace.workspaceDigest,
778
+ evidence,
779
+ };
780
+ await writeFile(temporary, `${canonicalize(record)}\n`, { mode: 0o600 });
781
+ await rename(temporary, target);
782
+ }
783
+ function evidenceLocationIdentity(item) {
784
+ return `${item.source}\0${item.location}`;
785
+ }
786
+ function buildEvidenceDelta(input) {
787
+ const baselineItems = input.baseline?.items ?? [];
788
+ const baselineByLocation = new Map(baselineItems.map((item) => [evidenceLocationIdentity(item), item]));
789
+ const currentByLocation = new Map(input.current.map((item) => [evidenceLocationIdentity(item), item]));
790
+ const currentById = new Map(input.current.map((item) => [item.id, item]));
791
+ const baselineById = new Map(baselineItems.map((item) => [item.id, item]));
792
+ const changedEvidenceRefs = input.current
793
+ .filter((item) => {
794
+ const prior = baselineByLocation.get(evidenceLocationIdentity(item));
795
+ return (!prior ||
796
+ prior.digest !== item.digest ||
797
+ prior.sensitivity !== item.sensitivity ||
798
+ canonicalDigest(prior.metric ?? null) !== canonicalDigest(item.metric ?? null));
799
+ })
800
+ .map((item) => item.id)
801
+ .sort();
802
+ const removedEvidenceRefs = baselineItems
803
+ .filter((item) => !currentByLocation.has(evidenceLocationIdentity(item)))
804
+ .map((item) => item.id)
805
+ .sort();
806
+ const requiredEvidenceRefs = [
807
+ ...new Set(input.requiredEvidenceRefs.flatMap((reference) => {
808
+ const current = currentById.get(reference);
809
+ if (current)
810
+ return [current.id];
811
+ const prior = baselineById.get(reference);
812
+ if (!prior)
813
+ return [];
814
+ return [currentByLocation.get(evidenceLocationIdentity(prior))?.id ?? prior.id];
815
+ })),
816
+ ].sort();
817
+ return {
818
+ mode: input.incremental ? (input.baseline ? 'standard' : 'baseline') : 'deep',
819
+ baselineFingerprint: input.baseline?.fingerprint ?? null,
820
+ changedEvidenceRefs,
821
+ requiredEvidenceRefs,
822
+ selectedEvidenceRefs: [...new Set([...changedEvidenceRefs, ...requiredEvidenceRefs])].sort(),
823
+ removedEvidenceRefs,
824
+ };
825
+ }
826
+ export async function collectOperatingEvidence(input) {
827
+ const started = Date.now();
828
+ const now = input.now ?? new Date();
829
+ const deadline = started + input.budgets.maxDurationMs;
830
+ const incrementalKey = canonicalDigest({
831
+ workspaceComponents: [input.workspace.controlRepository, ...input.workspace.components]
832
+ .map((component) => ({
833
+ componentId: component.componentId,
834
+ canonicalRemote: component.canonicalRemote,
835
+ configuredBranch: component.configuredBranch,
836
+ }))
837
+ .sort((left, right) => left.componentId.localeCompare(right.componentId)),
838
+ providers: [...new Set(input.providers)].sort(),
839
+ sensitivityCeiling: input.sensitivityCeiling,
840
+ budgets: input.budgets,
841
+ });
842
+ const baselineRecord = await readIncrementalEvidence(input, incrementalKey);
843
+ const roots = await readRoots(input.projectRoot, input.localRoot);
844
+ const raw = [];
845
+ const warnings = [];
846
+ let truncated = false;
847
+ if (input.providers.includes('repository') || input.providers.includes('planr')) {
848
+ const repository = await repositoryItems(roots, input.workspace, input.budgets, now, {
849
+ includeRepository: input.providers.includes('repository'),
850
+ includePlanr: input.providers.includes('planr'),
851
+ deadline,
852
+ });
853
+ raw.push(...repository.items);
854
+ truncated ||= repository.truncated;
855
+ }
856
+ if (input.providers.includes('git') && Date.now() < deadline) {
857
+ const git = await gitItems(roots, input.workspace, now, deadline);
858
+ raw.push(...git.items);
859
+ truncated ||= git.truncated;
860
+ }
861
+ if (input.providers.includes('github') && Date.now() < deadline) {
862
+ try {
863
+ const github = await collectGitHubEvidence({
864
+ projectRoot: input.projectRoot,
865
+ workspace: input.workspace,
866
+ now,
867
+ deadline,
868
+ budgets: input.budgets,
869
+ token: input.remote?.githubToken,
870
+ fetchImpl: input.remote?.fetchImpl,
871
+ });
872
+ raw.push(...github.items);
873
+ truncated ||= github.truncated;
874
+ if (github.items.length === 0) {
875
+ warnings.push('GitHub evidence found no supported repositories or records.');
876
+ }
877
+ }
878
+ catch {
879
+ warnings.push('GitHub evidence is unavailable or not authenticated.');
880
+ }
881
+ }
882
+ if (input.providers.includes('linear') && Date.now() < deadline) {
883
+ try {
884
+ const linear = await collectLinearEvidence({
885
+ projectRoot: input.projectRoot,
886
+ now,
887
+ deadline,
888
+ budgets: input.budgets,
889
+ token: input.remote?.linearToken,
890
+ fetchImpl: input.remote?.fetchImpl,
891
+ });
892
+ raw.push(...linear.items);
893
+ truncated ||= linear.truncated;
894
+ if (linear.items.length === 0) {
895
+ warnings.push('Linear evidence returned no configured teams, issues, or projects.');
896
+ }
897
+ }
898
+ catch {
899
+ warnings.push('Linear evidence is unavailable, not configured, or not authenticated.');
900
+ }
901
+ }
902
+ if (input.providers.includes('file-import') && Date.now() < deadline) {
903
+ const imported = await fileImportItems(input.projectRoot, roots, input.budgets, now, deadline, input.localRoot);
904
+ raw.push(...imported.items);
905
+ truncated ||= imported.truncated;
906
+ }
907
+ const rejected = raw.filter((item) => compareSensitivity(item.sensitivity, input.sensitivityCeiling) > 0);
908
+ if (rejected.length > 0) {
909
+ warnings.push(`${rejected.length} evidence item(s) exceeded the configured sensitivity ceiling.`);
910
+ }
911
+ const flattened = raw
912
+ .filter((item) => compareSensitivity(item.sensitivity, input.sensitivityCeiling) <= 0)
913
+ .slice(0, input.budgets.maxItems)
914
+ .map(sanitizeEvidenceItem);
915
+ if (Date.now() >= deadline) {
916
+ truncated = true;
917
+ warnings.push('Evidence collection reached the configured duration budget.');
918
+ }
919
+ const sourceIds = [...new Set(input.providers)].sort();
920
+ const delta = buildEvidenceDelta({
921
+ incremental: Boolean(input.incremental),
922
+ current: flattened,
923
+ baseline: baselineRecord?.evidence ?? null,
924
+ requiredEvidenceRefs: input.requiredEvidenceRefs ?? [],
925
+ });
926
+ const baselineSources = new Map((baselineRecord?.evidence.sources ?? []).map((source) => [source.id, source]));
927
+ const sources = sourceIds.map((id) => {
928
+ const items = flattened.filter((item) => item.source === id);
929
+ const fingerprint = canonicalDigest(evidenceFingerprintItems(items));
930
+ const prior = baselineSources.get(id);
931
+ return {
932
+ id,
933
+ fingerprint,
934
+ status: items.length === 0
935
+ ? 'unavailable'
936
+ : delta.mode === 'standard' && prior?.fingerprint === fingerprint
937
+ ? 'unchanged'
938
+ : delta.mode === 'standard'
939
+ ? 'partial'
940
+ : 'collected',
941
+ itemCount: items.length,
942
+ byteCount: items.reduce((sum, item) => sum + Buffer.byteLength(item.summary ?? ''), 0),
943
+ };
944
+ });
945
+ if (delta.mode === 'standard') {
946
+ warnings.push(`Standard evidence delta selected ${delta.changedEvidenceRefs.length} changed and ${delta.requiredEvidenceRefs.length} open-item-required evidence item(s).`);
947
+ }
948
+ const evidence = {
949
+ kind: 'operating-evidence',
950
+ schemaVersion: OPERATE_SCHEMA_VERSION,
951
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
952
+ cycleId: input.cycleId,
953
+ fingerprint: canonicalDigest({
954
+ workspaceDigest: input.workspace.workspaceDigest,
955
+ items: evidenceFingerprintItems(flattened),
956
+ sources,
957
+ delta,
958
+ }),
959
+ collectedAt: now.toISOString(),
960
+ truncated: truncated ||
961
+ flattened.length >= input.budgets.maxItems ||
962
+ sources.reduce((sum, source) => sum + source.byteCount, 0) >= input.budgets.maxBytes,
963
+ items: flattened,
964
+ delta,
965
+ sources,
966
+ warnings,
967
+ };
968
+ const validated = await assertOperatingArtifact('operating-evidence', evidence);
969
+ await writeIncrementalEvidence(input, incrementalKey, validated);
970
+ return validated;
971
+ }
972
+ /**
973
+ * Convert commercial numbers without reproducible query/window identity into
974
+ * governed gaps before any advisor can turn them into recommendations.
975
+ */
976
+ export async function unqualifiedCommercialEvidenceGaps(input) {
977
+ const unqualified = input.evidence.items
978
+ .filter((item) => item.claimTypes.includes('commercial-metric') &&
979
+ (!item.metric ||
980
+ !item.metric.identity.trim() ||
981
+ !item.metric.query.trim() ||
982
+ !validIsoWindow(item.metric.observedFrom, item.metric.observedTo)))
983
+ .sort((left, right) => left.source.localeCompare(right.source) ||
984
+ left.location.localeCompare(right.location) ||
985
+ left.id.localeCompare(right.id));
986
+ const gaps = unqualified.map((item, index) => ({
987
+ kind: 'operating-data-gap',
988
+ schemaVersion: OPERATE_SCHEMA_VERSION,
989
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
990
+ id: `GAP-${String(index + 1).padStart(3, '0')}`,
991
+ cycleId: input.cycleId,
992
+ question: `What reproducible metric query and observation window qualify ${item.location}?`,
993
+ reason: 'Numeric commercial evidence requires a metric identity, query identity, and bounded observation window before it can support an operating recommendation.',
994
+ unblocks: [],
995
+ affectedRoles: [
996
+ 'strategy-finance',
997
+ 'product-activation',
998
+ 'growth-market',
999
+ 'operations-customer',
1000
+ ],
1001
+ status: 'open',
1002
+ owner: input.owner,
1003
+ evidenceRefs: [item.id],
1004
+ createdAt: input.now,
1005
+ updatedAt: input.now,
1006
+ }));
1007
+ await Promise.all(gaps.map((gap) => assertOperatingArtifact('operating-data-gap', gap)));
1008
+ return gaps;
1009
+ }
1010
+ export function evidenceProjectionSources(evidence) {
1011
+ return evidence.sources.map((source) => ({
1012
+ id: source.id,
1013
+ freshness: evidence.items.some((item) => item.source === source.id && item.freshness === 'stale')
1014
+ ? 'stale'
1015
+ : evidence.items.some((item) => item.source === source.id)
1016
+ ? 'fresh'
1017
+ : 'unknown',
1018
+ status: source.status,
1019
+ itemCount: source.itemCount,
1020
+ }));
1021
+ }
1022
+ //# sourceMappingURL=evidence.js.map