openplanr 1.18.0 → 1.20.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.
- package/dist/cli/commands/backlog.d.ts.map +1 -1
- package/dist/cli/commands/backlog.js +3 -0
- package/dist/cli/commands/backlog.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +3 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/epic.d.ts.map +1 -1
- package/dist/cli/commands/epic.js +2 -0
- package/dist/cli/commands/epic.js.map +1 -1
- package/dist/cli/commands/feature.d.ts.map +1 -1
- package/dist/cli/commands/feature.js +2 -0
- package/dist/cli/commands/feature.js.map +1 -1
- package/dist/cli/commands/operate.d.ts +5 -2
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +10 -101
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +2 -0
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/quick.d.ts.map +1 -1
- package/dist/cli/commands/quick.js +2 -0
- package/dist/cli/commands/quick.js.map +1 -1
- package/dist/cli/commands/refine.d.ts.map +1 -1
- package/dist/cli/commands/refine.js +2 -0
- package/dist/cli/commands/refine.js.map +1 -1
- package/dist/cli/commands/revise.d.ts.map +1 -1
- package/dist/cli/commands/revise.js +2 -0
- package/dist/cli/commands/revise.js.map +1 -1
- package/dist/cli/commands/spec.d.ts.map +1 -1
- package/dist/cli/commands/spec.js +2 -0
- package/dist/cli/commands/spec.js.map +1 -1
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +2 -0
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/commands/story.d.ts.map +1 -1
- package/dist/cli/commands/story.js +3 -0
- package/dist/cli/commands/story.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/services/ai-service.d.ts +3 -5
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +7 -1
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/credentials-service.d.ts.map +1 -1
- package/dist/services/credentials-service.js +2 -0
- package/dist/services/credentials-service.js.map +1 -1
- package/dist/services/deprecation-notices.d.ts +5 -0
- package/dist/services/deprecation-notices.d.ts.map +1 -0
- package/dist/services/deprecation-notices.js +6 -0
- package/dist/services/deprecation-notices.js.map +1 -0
- package/dist/services/operate/advisors.d.ts +146 -108
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +178 -448
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +50 -1
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/config.d.ts +28 -52
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +68 -203
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +15 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +142 -32
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +22 -62
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +269 -191
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +9 -78
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +0 -1194
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +15 -231
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/integrity.d.ts +81 -0
- package/dist/services/operate/integrity.d.ts.map +1 -0
- package/dist/services/operate/integrity.js +152 -0
- package/dist/services/operate/integrity.js.map +1 -0
- package/dist/services/operate/interaction/answer-service.d.ts +0 -10
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +63 -43
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +105 -21
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +16 -1
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +45 -39
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/session-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/session-service.js +16 -11
- package/dist/services/operate/interaction/session-service.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +16 -6
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/lifecycle.d.ts.map +1 -1
- package/dist/services/operate/lifecycle.js +17 -1
- package/dist/services/operate/lifecycle.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +95 -276
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +73 -55
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +91 -67
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/projection-persistence.d.ts +35 -7
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +142 -53
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts +9 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +22 -12
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -41
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +7 -45
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/redaction.d.ts +3 -3
- package/dist/services/operate/redaction.js +4 -4
- package/dist/services/operate/redaction.js.map +1 -1
- package/dist/services/operate/reports.d.ts +20 -1
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +108 -4
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/routes.d.ts +84 -0
- package/dist/services/operate/routes.d.ts.map +1 -1
- package/dist/services/operate/routes.js +519 -10
- package/dist/services/operate/routes.js.map +1 -1
- package/dist/services/operate/types.d.ts +48 -133
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +0 -1
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +0 -2
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.js +2 -2
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +3 -3
- package/dist/services/operate/evidence-classifications.d.ts +0 -59
- package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
- package/dist/services/operate/evidence-classifications.js +0 -132
- package/dist/services/operate/evidence-classifications.js.map +0 -1
- package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
- package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
- package/dist/services/operate/evidence-diagnostics.js +0 -152
- package/dist/services/operate/evidence-diagnostics.js.map +0 -1
- package/dist/services/operate/evidence-readiness.d.ts +0 -8
- package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
- package/dist/services/operate/evidence-readiness.js +0 -84
- package/dist/services/operate/evidence-readiness.js.map +0 -1
|
@@ -1,47 +1,3 @@
|
|
|
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 { OperatingEventStore } from './event-store.js';
|
|
7
|
-
import { createEvidenceDiagnostic } from './evidence-diagnostics.js';
|
|
8
|
-
import { readImportedEvidenceFile } from './evidence-import.js';
|
|
9
|
-
import { assertOperatingArtifact } from './protocol.js';
|
|
10
|
-
import { executeGitHubReadOnly, executeGitReadOnly, executeLinearReadOnlyQuery, executeRestReadOnlyJson, } from './read-only-providers.js';
|
|
11
|
-
import { compareSensitivity, detectSecretMetadata, sanitizeEvidenceItem } from './redaction.js';
|
|
12
|
-
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
13
|
-
import { isPathInside, resolveOperatingPaths } from './workspace.js';
|
|
14
|
-
const TEXT_EXTENSIONS = new Set([
|
|
15
|
-
'.c',
|
|
16
|
-
'.cc',
|
|
17
|
-
'.css',
|
|
18
|
-
'.csv',
|
|
19
|
-
'.go',
|
|
20
|
-
'.graphql',
|
|
21
|
-
'.h',
|
|
22
|
-
'.html',
|
|
23
|
-
'.java',
|
|
24
|
-
'.js',
|
|
25
|
-
'.json',
|
|
26
|
-
'.jsx',
|
|
27
|
-
'.kt',
|
|
28
|
-
'.md',
|
|
29
|
-
'.mjs',
|
|
30
|
-
'.py',
|
|
31
|
-
'.rb',
|
|
32
|
-
'.rs',
|
|
33
|
-
'.sh',
|
|
34
|
-
'.sql',
|
|
35
|
-
'.swift',
|
|
36
|
-
'.toml',
|
|
37
|
-
'.ts',
|
|
38
|
-
'.tsx',
|
|
39
|
-
'.yaml',
|
|
40
|
-
'.yml',
|
|
41
|
-
]);
|
|
42
|
-
function evidenceId(source, identity) {
|
|
43
|
-
return `EVD-${source}-${canonicalDigest(identity).slice('sha256:'.length, 19)}`;
|
|
44
|
-
}
|
|
45
1
|
export function evidenceFingerprintItems(items) {
|
|
46
2
|
return items
|
|
47
3
|
.map(({ id, digest, sensitivity }) => ({ id, digest, sensitivity }))
|
|
@@ -49,1037 +5,6 @@ export function evidenceFingerprintItems(items) {
|
|
|
49
5
|
left.digest.localeCompare(right.digest) ||
|
|
50
6
|
left.sensitivity.localeCompare(right.sensitivity));
|
|
51
7
|
}
|
|
52
|
-
function ageFreshness(collectedAt, now, ttlHours = 168) {
|
|
53
|
-
return now.getTime() - Date.parse(collectedAt) <= ttlHours * 60 * 60 * 1_000 ? 'fresh' : 'stale';
|
|
54
|
-
}
|
|
55
|
-
async function readRoots(projectRoot, localRoot) {
|
|
56
|
-
const candidates = [projectRoot, await realpath(projectRoot).catch(() => projectRoot)];
|
|
57
|
-
for (const candidate of [...new Set(candidates)]) {
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(await readFile(resolveOperatingPaths(candidate, { localRoot }).roots, 'utf8'));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
// Invocation paths can differ from their canonical real path on systems
|
|
63
|
-
// such as macOS where /var resolves through /private/var.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
throw new OperateError('E_OPERATE_NOT_INITIALIZED', 'Machine-local workspace roots are missing; run `planr operate init` again.');
|
|
67
|
-
}
|
|
68
|
-
function descriptorById(workspace) {
|
|
69
|
-
return new Map([workspace.controlRepository, ...workspace.components].map((entry) => [
|
|
70
|
-
entry.componentId,
|
|
71
|
-
entry,
|
|
72
|
-
]));
|
|
73
|
-
}
|
|
74
|
-
async function repositoryItems(roots, workspace, budgets, now, options) {
|
|
75
|
-
const descriptors = descriptorById(workspace);
|
|
76
|
-
const results = [];
|
|
77
|
-
let files = 0;
|
|
78
|
-
let bytes = 0;
|
|
79
|
-
let truncated = false;
|
|
80
|
-
for (const [componentId, root] of Object.entries(roots.roots).sort()) {
|
|
81
|
-
if (Date.now() >= options.deadline) {
|
|
82
|
-
truncated = true;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
const descriptor = descriptors.get(componentId);
|
|
86
|
-
if (!descriptor)
|
|
87
|
-
continue;
|
|
88
|
-
const canonicalRoot = await realpath(root);
|
|
89
|
-
const names = (await executeGitReadOnly(canonicalRoot, ['ls-files', '-z']))
|
|
90
|
-
.split('\0')
|
|
91
|
-
.filter(Boolean)
|
|
92
|
-
.filter((name) => TEXT_EXTENSIONS.has(path.extname(name).toLowerCase()))
|
|
93
|
-
.sort();
|
|
94
|
-
for (const relativePath of names) {
|
|
95
|
-
if (Date.now() >= options.deadline ||
|
|
96
|
-
files >= budgets.maxFiles ||
|
|
97
|
-
results.length >= budgets.maxItems) {
|
|
98
|
-
truncated = true;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
const isPlanr = relativePath.startsWith('.planr/');
|
|
102
|
-
if ((!options.includeRepository && !isPlanr) ||
|
|
103
|
-
(isPlanr && !options.includePlanr && !options.includeRepository)) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const target = path.join(canonicalRoot, relativePath);
|
|
107
|
-
const directInfo = await lstat(target).catch(() => null);
|
|
108
|
-
if (!directInfo)
|
|
109
|
-
continue;
|
|
110
|
-
const resolvedTarget = await realpath(target).catch(() => null);
|
|
111
|
-
if (!resolvedTarget)
|
|
112
|
-
continue;
|
|
113
|
-
if (!isPathInside(canonicalRoot, resolvedTarget)) {
|
|
114
|
-
throw new OperateError('E_OPERATE_PATH_ESCAPE', `Tracked evidence path follows a symlink outside its repository: ${componentId}/${relativePath}.`);
|
|
115
|
-
}
|
|
116
|
-
if (directInfo.isSymbolicLink())
|
|
117
|
-
continue;
|
|
118
|
-
const info = await stat(resolvedTarget).catch(() => null);
|
|
119
|
-
if (!info?.isFile() || info.size > budgets.maxItemBytes)
|
|
120
|
-
continue;
|
|
121
|
-
if (bytes + info.size > budgets.maxBytes) {
|
|
122
|
-
truncated = true;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
const content = await readFile(resolvedTarget, 'utf8').catch(() => null);
|
|
126
|
-
if (content === null || content.includes('\0'))
|
|
127
|
-
continue;
|
|
128
|
-
files += 1;
|
|
129
|
-
bytes += Buffer.byteLength(content);
|
|
130
|
-
const collectedAt = now.toISOString();
|
|
131
|
-
const repository = {
|
|
132
|
-
componentId,
|
|
133
|
-
canonicalRemote: `https://${descriptor.canonicalRemote}`,
|
|
134
|
-
revision: descriptor.pinnedRevision,
|
|
135
|
-
configuredBranch: descriptor.configuredBranch,
|
|
136
|
-
dirtyFingerprint: descriptor.dirtyFingerprint,
|
|
137
|
-
relativePath,
|
|
138
|
-
digest: canonicalDigest(content),
|
|
139
|
-
freshness: ageFreshness(collectedAt, now),
|
|
140
|
-
sensitivity: 'internal',
|
|
141
|
-
collectedAt,
|
|
142
|
-
};
|
|
143
|
-
if (options.includeRepository && results.length < budgets.maxItems) {
|
|
144
|
-
results.push({
|
|
145
|
-
id: evidenceId('repository', `${componentId}:${relativePath}:${repository.digest}`),
|
|
146
|
-
source: 'repository',
|
|
147
|
-
location: `${componentId}/${relativePath}`,
|
|
148
|
-
content,
|
|
149
|
-
collectedAt,
|
|
150
|
-
observedFrom: null,
|
|
151
|
-
observedTo: collectedAt,
|
|
152
|
-
freshness: repository.freshness,
|
|
153
|
-
sensitivity: repository.sensitivity,
|
|
154
|
-
claimTypes: ['code', 'architecture'],
|
|
155
|
-
quality: 'observed',
|
|
156
|
-
coverage: 'partial',
|
|
157
|
-
repository,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (isPlanr && options.includePlanr && results.length < budgets.maxItems) {
|
|
161
|
-
const planrClaims = [
|
|
162
|
-
'planning',
|
|
163
|
-
...(relativePath.includes('/stories/') || relativePath.includes('/specs/')
|
|
164
|
-
? ['user-journey']
|
|
165
|
-
: []),
|
|
166
|
-
...(relativePath.includes('outcome') || relativePath.includes('provenance')
|
|
167
|
-
? ['outcome']
|
|
168
|
-
: []),
|
|
169
|
-
...(relativePath.includes('/support') ? ['support', 'operations'] : []),
|
|
170
|
-
];
|
|
171
|
-
results.push({
|
|
172
|
-
id: evidenceId('planr', `${componentId}:${relativePath}:${repository.digest}`),
|
|
173
|
-
source: 'planr',
|
|
174
|
-
location: `${componentId}/${relativePath}`,
|
|
175
|
-
content,
|
|
176
|
-
collectedAt,
|
|
177
|
-
observedFrom: null,
|
|
178
|
-
observedTo: collectedAt,
|
|
179
|
-
freshness: repository.freshness,
|
|
180
|
-
sensitivity: repository.sensitivity,
|
|
181
|
-
claimTypes: planrClaims,
|
|
182
|
-
quality: 'verified',
|
|
183
|
-
coverage: 'partial',
|
|
184
|
-
repository,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return { items: results, truncated };
|
|
190
|
-
}
|
|
191
|
-
async function gitItems(roots, workspace, now, deadline) {
|
|
192
|
-
const descriptors = descriptorById(workspace);
|
|
193
|
-
const results = [];
|
|
194
|
-
for (const [componentId, root] of Object.entries(roots.roots).sort()) {
|
|
195
|
-
const remaining = deadline - Date.now();
|
|
196
|
-
if (remaining <= 0)
|
|
197
|
-
return { items: results, truncated: true };
|
|
198
|
-
const descriptor = descriptors.get(componentId);
|
|
199
|
-
if (!descriptor)
|
|
200
|
-
continue;
|
|
201
|
-
// An unborn Git branch has no history to cite. The workspace manifest uses
|
|
202
|
-
// Git's null OID for that state; do not manufacture a change-history item
|
|
203
|
-
// that could incorrectly satisfy an advisor readiness requirement.
|
|
204
|
-
if (/^0{40,64}$/u.test(descriptor.pinnedRevision))
|
|
205
|
-
continue;
|
|
206
|
-
const content = await executeGitReadOnly(root, [
|
|
207
|
-
'log',
|
|
208
|
-
'--since=30.days',
|
|
209
|
-
'--max-count=200',
|
|
210
|
-
'--date=iso-strict',
|
|
211
|
-
'--pretty=format:%H%x09%ad%x09%s',
|
|
212
|
-
], { timeoutMs: remaining });
|
|
213
|
-
const collectedAt = now.toISOString();
|
|
214
|
-
results.push({
|
|
215
|
-
id: evidenceId('git', `${componentId}:${descriptor.pinnedRevision}:${content}`),
|
|
216
|
-
source: 'git',
|
|
217
|
-
location: `${componentId}/history`,
|
|
218
|
-
content,
|
|
219
|
-
collectedAt,
|
|
220
|
-
observedFrom: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1_000).toISOString(),
|
|
221
|
-
observedTo: collectedAt,
|
|
222
|
-
freshness: 'fresh',
|
|
223
|
-
sensitivity: 'internal',
|
|
224
|
-
claimTypes: ['change-history'],
|
|
225
|
-
quality: 'verified',
|
|
226
|
-
coverage: 'partial',
|
|
227
|
-
repository: {
|
|
228
|
-
componentId,
|
|
229
|
-
canonicalRemote: `https://${descriptor.canonicalRemote}`,
|
|
230
|
-
revision: descriptor.pinnedRevision,
|
|
231
|
-
configuredBranch: descriptor.configuredBranch,
|
|
232
|
-
dirtyFingerprint: descriptor.dirtyFingerprint,
|
|
233
|
-
relativePath: '.git',
|
|
234
|
-
digest: canonicalDigest(content),
|
|
235
|
-
freshness: 'fresh',
|
|
236
|
-
sensitivity: 'internal',
|
|
237
|
-
collectedAt,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
return { items: results, truncated: false };
|
|
242
|
-
}
|
|
243
|
-
function boundedRecordsContent(key, records, maxBytes) {
|
|
244
|
-
const selected = [...records];
|
|
245
|
-
let content = canonicalize({ [key]: selected });
|
|
246
|
-
while (selected.length > 0 && Buffer.byteLength(content) > maxBytes) {
|
|
247
|
-
selected.pop();
|
|
248
|
-
content = canonicalize({ [key]: selected });
|
|
249
|
-
}
|
|
250
|
-
if (Buffer.byteLength(content) > maxBytes) {
|
|
251
|
-
throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', `${key} evidence cannot fit inside the configured item limit.`);
|
|
252
|
-
}
|
|
253
|
-
return {
|
|
254
|
-
content,
|
|
255
|
-
records: selected.length,
|
|
256
|
-
truncated: selected.length < records.length,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
function githubIdentity(canonicalRemote) {
|
|
260
|
-
let url;
|
|
261
|
-
try {
|
|
262
|
-
url = new URL(canonicalRemote.includes('://') ? canonicalRemote : `https://${canonicalRemote}`);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
if (url.protocol !== 'https:' ||
|
|
268
|
-
url.hostname.toLowerCase() !== 'github.com' ||
|
|
269
|
-
url.username ||
|
|
270
|
-
url.password ||
|
|
271
|
-
(url.port && url.port !== '443')) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
const segments = url.pathname
|
|
275
|
-
.replace(/\.git$/i, '')
|
|
276
|
-
.split('/')
|
|
277
|
-
.filter(Boolean);
|
|
278
|
-
if (segments.length !== 2 || segments.some((segment) => !/^[A-Za-z0-9_.-]+$/.test(segment))) {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
return { owner: segments[0], repository: segments[1] };
|
|
282
|
-
}
|
|
283
|
-
async function githubPages(input) {
|
|
284
|
-
const results = [];
|
|
285
|
-
const pageSize = Math.min(100, Math.max(1, input.maxRecords));
|
|
286
|
-
const pageLimit = Math.min(5, Math.ceil(input.maxRecords / pageSize));
|
|
287
|
-
if (input.maxBytes < 1_024 || pageLimit < 1)
|
|
288
|
-
return results;
|
|
289
|
-
const perPageBytes = Math.max(1_024, Math.floor(input.maxBytes / pageLimit));
|
|
290
|
-
for (let page = 1; page <= pageLimit && results.length < input.maxRecords; page += 1) {
|
|
291
|
-
const remaining = input.deadline - Date.now();
|
|
292
|
-
if (remaining <= 0)
|
|
293
|
-
break;
|
|
294
|
-
const endpoint = new URL(input.endpoint);
|
|
295
|
-
endpoint.searchParams.set('per_page', String(pageSize));
|
|
296
|
-
endpoint.searchParams.set('page', String(page));
|
|
297
|
-
const response = await executeRestReadOnlyJson(endpoint, {
|
|
298
|
-
method: 'GET',
|
|
299
|
-
allowedHosts: ['api.github.com'],
|
|
300
|
-
timeoutMs: remaining,
|
|
301
|
-
maxBytes: Math.min(perPageBytes, 5 * 1024 * 1024),
|
|
302
|
-
fetchImpl: input.fetchImpl,
|
|
303
|
-
headers: {
|
|
304
|
-
accept: 'application/vnd.github+json',
|
|
305
|
-
authorization: `Bearer ${input.token}`,
|
|
306
|
-
'x-github-api-version': '2022-11-28',
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
const pageRecords = input.select(response);
|
|
310
|
-
const rawPageRecords = Array.isArray(response)
|
|
311
|
-
? response.length
|
|
312
|
-
: response &&
|
|
313
|
-
typeof response === 'object' &&
|
|
314
|
-
Array.isArray(response.check_runs)
|
|
315
|
-
? response.check_runs.length
|
|
316
|
-
: pageRecords.length;
|
|
317
|
-
results.push(...pageRecords.slice(0, input.maxRecords - results.length));
|
|
318
|
-
if (rawPageRecords < pageSize)
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
return results;
|
|
322
|
-
}
|
|
323
|
-
export async function collectGitHubEvidence(input) {
|
|
324
|
-
const token = input.token?.trim() ||
|
|
325
|
-
process.env.GH_TOKEN?.trim() ||
|
|
326
|
-
process.env.GITHUB_TOKEN?.trim() ||
|
|
327
|
-
(await executeGitHubReadOnly(input.projectRoot, ['auth', 'token', '--hostname', 'github.com'], { allowedHosts: ['github.com'], timeoutMs: input.deadline - Date.now() })).trim();
|
|
328
|
-
if (!token) {
|
|
329
|
-
throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', 'GitHub credentials are unavailable.');
|
|
330
|
-
}
|
|
331
|
-
const descriptors = [input.workspace.controlRepository, ...input.workspace.components];
|
|
332
|
-
const items = [];
|
|
333
|
-
let truncated = false;
|
|
334
|
-
let remainingBytes = input.budgets.maxBytes;
|
|
335
|
-
const maxRecords = Math.min(100, Math.max(1, Math.floor(input.budgets.maxItems / 4)));
|
|
336
|
-
const categoryResponseBytes = Math.max(1_024, Math.floor(input.budgets.maxBytes / 4));
|
|
337
|
-
for (const descriptor of descriptors) {
|
|
338
|
-
const identity = githubIdentity(descriptor.canonicalRemote);
|
|
339
|
-
if (!identity)
|
|
340
|
-
continue;
|
|
341
|
-
if (Date.now() >= input.deadline || items.length >= input.budgets.maxItems) {
|
|
342
|
-
truncated = true;
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
const base = `https://api.github.com/repos/${encodeURIComponent(identity.owner)}/${encodeURIComponent(identity.repository)}`;
|
|
346
|
-
const categories = [
|
|
347
|
-
{
|
|
348
|
-
id: 'issues',
|
|
349
|
-
endpoint: `${base}/issues?state=all`,
|
|
350
|
-
claims: ['customer-signal', 'delivery'],
|
|
351
|
-
select: (value) => (Array.isArray(value) ? value : [])
|
|
352
|
-
.filter((record) => Boolean(record) && typeof record === 'object' && !('pull_request' in record))
|
|
353
|
-
.map((record) => ({
|
|
354
|
-
number: record.number ?? null,
|
|
355
|
-
title: record.title ?? null,
|
|
356
|
-
state: record.state ?? null,
|
|
357
|
-
createdAt: record.created_at ?? null,
|
|
358
|
-
updatedAt: record.updated_at ?? null,
|
|
359
|
-
labels: Array.isArray(record.labels)
|
|
360
|
-
? record.labels
|
|
361
|
-
.map((label) => label && typeof label === 'object'
|
|
362
|
-
? label.name
|
|
363
|
-
: label)
|
|
364
|
-
.filter((label) => typeof label === 'string')
|
|
365
|
-
.sort()
|
|
366
|
-
: [],
|
|
367
|
-
})),
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
id: 'pull-requests',
|
|
371
|
-
endpoint: `${base}/pulls?state=all`,
|
|
372
|
-
claims: ['delivery', 'change-history'],
|
|
373
|
-
select: (value) => (Array.isArray(value) ? value : [])
|
|
374
|
-
.filter((record) => Boolean(record) && typeof record === 'object')
|
|
375
|
-
.map((record) => ({
|
|
376
|
-
number: record.number ?? null,
|
|
377
|
-
title: record.title ?? null,
|
|
378
|
-
state: record.state ?? null,
|
|
379
|
-
draft: record.draft ?? null,
|
|
380
|
-
createdAt: record.created_at ?? null,
|
|
381
|
-
updatedAt: record.updated_at ?? null,
|
|
382
|
-
mergedAt: record.merged_at ?? null,
|
|
383
|
-
})),
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
id: 'releases',
|
|
387
|
-
endpoint: `${base}/releases`,
|
|
388
|
-
claims: ['delivery', 'change-history'],
|
|
389
|
-
select: (value) => (Array.isArray(value) ? value : [])
|
|
390
|
-
.filter((record) => Boolean(record) && typeof record === 'object')
|
|
391
|
-
.map((record) => ({
|
|
392
|
-
id: record.id ?? null,
|
|
393
|
-
tag: record.tag_name ?? null,
|
|
394
|
-
name: record.name ?? null,
|
|
395
|
-
draft: record.draft ?? null,
|
|
396
|
-
prerelease: record.prerelease ?? null,
|
|
397
|
-
createdAt: record.created_at ?? null,
|
|
398
|
-
publishedAt: record.published_at ?? null,
|
|
399
|
-
})),
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
id: 'checks',
|
|
403
|
-
endpoint: `${base}/commits/${encodeURIComponent(descriptor.pinnedRevision)}/check-runs`,
|
|
404
|
-
claims: ['delivery', 'reliability'],
|
|
405
|
-
select: (value) => {
|
|
406
|
-
if (!value || typeof value !== 'object')
|
|
407
|
-
return [];
|
|
408
|
-
const records = value.check_runs;
|
|
409
|
-
return (Array.isArray(records) ? records : [])
|
|
410
|
-
.filter((record) => Boolean(record) && typeof record === 'object')
|
|
411
|
-
.map((record) => ({
|
|
412
|
-
id: record.id ?? null,
|
|
413
|
-
name: record.name ?? null,
|
|
414
|
-
status: record.status ?? null,
|
|
415
|
-
conclusion: record.conclusion ?? null,
|
|
416
|
-
startedAt: record.started_at ?? null,
|
|
417
|
-
completedAt: record.completed_at ?? null,
|
|
418
|
-
}));
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
];
|
|
422
|
-
for (const category of categories) {
|
|
423
|
-
if (Date.now() >= input.deadline ||
|
|
424
|
-
items.length >= input.budgets.maxItems ||
|
|
425
|
-
remainingBytes < 1_024) {
|
|
426
|
-
truncated = true;
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
const records = (await githubPages({
|
|
430
|
-
token,
|
|
431
|
-
endpoint: category.endpoint,
|
|
432
|
-
deadline: input.deadline,
|
|
433
|
-
maxRecords,
|
|
434
|
-
maxBytes: Math.min(remainingBytes, categoryResponseBytes),
|
|
435
|
-
fetchImpl: input.fetchImpl,
|
|
436
|
-
select: category.select,
|
|
437
|
-
}));
|
|
438
|
-
const bounded = boundedRecordsContent(category.id, records, Math.min(input.budgets.maxItemBytes, remainingBytes));
|
|
439
|
-
truncated ||= bounded.truncated || records.length >= maxRecords;
|
|
440
|
-
remainingBytes -= Buffer.byteLength(bounded.content);
|
|
441
|
-
if (bounded.records === 0)
|
|
442
|
-
continue;
|
|
443
|
-
const collectedAt = input.now.toISOString();
|
|
444
|
-
items.push({
|
|
445
|
-
id: evidenceId('github', `${descriptor.componentId}:${category.id}:${bounded.content}`),
|
|
446
|
-
source: 'github',
|
|
447
|
-
location: `${descriptor.componentId}/${category.id}`,
|
|
448
|
-
content: bounded.content,
|
|
449
|
-
collectedAt,
|
|
450
|
-
observedFrom: null,
|
|
451
|
-
observedTo: collectedAt,
|
|
452
|
-
freshness: 'fresh',
|
|
453
|
-
sensitivity: 'confidential',
|
|
454
|
-
claimTypes: category.claims,
|
|
455
|
-
quality: 'observed',
|
|
456
|
-
coverage: bounded.truncated ? 'partial' : 'complete',
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return { items, truncated };
|
|
461
|
-
}
|
|
462
|
-
async function linearConnectionPages(input) {
|
|
463
|
-
const records = [];
|
|
464
|
-
let after = null;
|
|
465
|
-
const pageSize = Math.min(100, Math.max(1, input.maxRecords));
|
|
466
|
-
const pageLimit = Math.min(5, Math.ceil(input.maxRecords / pageSize));
|
|
467
|
-
const perPageBytes = Math.max(1_024, Math.floor(input.maxBytes / Math.max(1, pageLimit)));
|
|
468
|
-
for (let page = 0; page < pageLimit && records.length < input.maxRecords; page += 1) {
|
|
469
|
-
const remaining = input.deadline - Date.now();
|
|
470
|
-
if (remaining <= 0)
|
|
471
|
-
break;
|
|
472
|
-
const data = await executeLinearReadOnlyQuery({
|
|
473
|
-
token: input.token,
|
|
474
|
-
query: input.query,
|
|
475
|
-
variables: {
|
|
476
|
-
...input.variables,
|
|
477
|
-
first: pageSize,
|
|
478
|
-
after,
|
|
479
|
-
},
|
|
480
|
-
timeoutMs: remaining,
|
|
481
|
-
maxBytes: Math.min(perPageBytes, 5 * 1024 * 1024),
|
|
482
|
-
allowedHosts: ['api.linear.app'],
|
|
483
|
-
fetchImpl: input.fetchImpl,
|
|
484
|
-
});
|
|
485
|
-
const connection = data[input.connection];
|
|
486
|
-
const nodes = Array.isArray(connection?.nodes) ? connection.nodes : [];
|
|
487
|
-
records.push(...nodes.slice(0, input.maxRecords - records.length));
|
|
488
|
-
if (!connection?.pageInfo?.hasNextPage || !connection.pageInfo.endCursor)
|
|
489
|
-
break;
|
|
490
|
-
after = connection.pageInfo.endCursor;
|
|
491
|
-
}
|
|
492
|
-
return records;
|
|
493
|
-
}
|
|
494
|
-
export async function collectLinearEvidence(input) {
|
|
495
|
-
const token = input.token?.trim() || (await resolveApiKey('linear'))?.trim();
|
|
496
|
-
if (!token) {
|
|
497
|
-
throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', 'Linear credentials are unavailable.');
|
|
498
|
-
}
|
|
499
|
-
const config = input.teamIds ? null : await loadConfig(input.projectRoot).catch(() => null);
|
|
500
|
-
const teamIds = [
|
|
501
|
-
...new Set((input.teamIds ?? [
|
|
502
|
-
config?.linear?.teamId,
|
|
503
|
-
...(config?.linear?.teams ?? []).map((team) => team.id),
|
|
504
|
-
]).filter((value) => Boolean(value))),
|
|
505
|
-
];
|
|
506
|
-
const maxRecords = Math.min(100, Math.max(1, Math.floor(input.budgets.maxItems / 3)));
|
|
507
|
-
const connectionBudget = Math.max(1_024, Math.floor(input.budgets.maxBytes / 3));
|
|
508
|
-
const teams = await linearConnectionPages({
|
|
509
|
-
token,
|
|
510
|
-
query: `query OperatingTeams($first: Int!, $after: String) {
|
|
511
|
-
teams(first: $first, after: $after) {
|
|
512
|
-
nodes { id key name }
|
|
513
|
-
pageInfo { hasNextPage endCursor }
|
|
514
|
-
}
|
|
515
|
-
}`,
|
|
516
|
-
connection: 'teams',
|
|
517
|
-
deadline: input.deadline,
|
|
518
|
-
maxRecords,
|
|
519
|
-
maxBytes: connectionBudget,
|
|
520
|
-
fetchImpl: input.fetchImpl,
|
|
521
|
-
});
|
|
522
|
-
const issues = await linearConnectionPages({
|
|
523
|
-
token,
|
|
524
|
-
query: teamIds.length > 0
|
|
525
|
-
? `query OperatingIssues($first: Int!, $after: String, $teamIds: [ID!]) {
|
|
526
|
-
issues(first: $first, after: $after, filter: { team: { id: { in: $teamIds } } }) {
|
|
527
|
-
nodes {
|
|
528
|
-
id identifier title updatedAt priority
|
|
529
|
-
team { id key }
|
|
530
|
-
state { name }
|
|
531
|
-
labels { nodes { name } }
|
|
532
|
-
}
|
|
533
|
-
pageInfo { hasNextPage endCursor }
|
|
534
|
-
}
|
|
535
|
-
}`
|
|
536
|
-
: `query OperatingIssues($first: Int!, $after: String) {
|
|
537
|
-
issues(first: $first, after: $after) {
|
|
538
|
-
nodes {
|
|
539
|
-
id identifier title updatedAt priority
|
|
540
|
-
team { id key }
|
|
541
|
-
state { name }
|
|
542
|
-
labels { nodes { name } }
|
|
543
|
-
}
|
|
544
|
-
pageInfo { hasNextPage endCursor }
|
|
545
|
-
}
|
|
546
|
-
}`,
|
|
547
|
-
connection: 'issues',
|
|
548
|
-
variables: teamIds.length > 0 ? { teamIds } : {},
|
|
549
|
-
deadline: input.deadline,
|
|
550
|
-
maxRecords,
|
|
551
|
-
maxBytes: connectionBudget,
|
|
552
|
-
fetchImpl: input.fetchImpl,
|
|
553
|
-
});
|
|
554
|
-
const projects = await linearConnectionPages({
|
|
555
|
-
token,
|
|
556
|
-
query: `query OperatingProjects($first: Int!, $after: String) {
|
|
557
|
-
projects(first: $first, after: $after) {
|
|
558
|
-
nodes {
|
|
559
|
-
id name updatedAt
|
|
560
|
-
status { name }
|
|
561
|
-
teams { nodes { id key name } }
|
|
562
|
-
}
|
|
563
|
-
pageInfo { hasNextPage endCursor }
|
|
564
|
-
}
|
|
565
|
-
}`,
|
|
566
|
-
connection: 'projects',
|
|
567
|
-
deadline: input.deadline,
|
|
568
|
-
maxRecords,
|
|
569
|
-
maxBytes: connectionBudget,
|
|
570
|
-
fetchImpl: input.fetchImpl,
|
|
571
|
-
});
|
|
572
|
-
const selectedProjects = teamIds.length === 0
|
|
573
|
-
? projects
|
|
574
|
-
: projects.filter((project) => (project.teams?.nodes ?? []).some((team) => team.id && teamIds.includes(team.id)));
|
|
575
|
-
const records = [
|
|
576
|
-
{
|
|
577
|
-
id: 'teams',
|
|
578
|
-
values: teams.map((team) => ({
|
|
579
|
-
id: team.id ?? null,
|
|
580
|
-
key: team.key ?? null,
|
|
581
|
-
name: team.name ?? null,
|
|
582
|
-
})),
|
|
583
|
-
claims: ['planning', 'operations'],
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
id: 'issues',
|
|
587
|
-
values: issues.map((issue) => ({
|
|
588
|
-
id: issue.id ?? null,
|
|
589
|
-
identifier: issue.identifier ?? null,
|
|
590
|
-
title: issue.title ?? null,
|
|
591
|
-
updatedAt: issue.updatedAt ?? null,
|
|
592
|
-
priority: issue.priority ?? null,
|
|
593
|
-
team: issue.team ? { id: issue.team.id ?? null, key: issue.team.key ?? null } : null,
|
|
594
|
-
state: issue.state?.name ?? null,
|
|
595
|
-
labels: (issue.labels?.nodes ?? [])
|
|
596
|
-
.map((label) => label.name)
|
|
597
|
-
.filter((label) => Boolean(label))
|
|
598
|
-
.sort(),
|
|
599
|
-
})),
|
|
600
|
-
claims: ['customer-signal', 'delivery', 'planning'],
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
id: 'projects',
|
|
604
|
-
values: selectedProjects.map((project) => ({
|
|
605
|
-
id: project.id ?? null,
|
|
606
|
-
name: project.name ?? null,
|
|
607
|
-
updatedAt: project.updatedAt ?? null,
|
|
608
|
-
status: project.status?.name ?? null,
|
|
609
|
-
teams: (project.teams?.nodes ?? [])
|
|
610
|
-
.map((team) => ({
|
|
611
|
-
id: team.id ?? null,
|
|
612
|
-
key: team.key ?? null,
|
|
613
|
-
name: team.name ?? null,
|
|
614
|
-
}))
|
|
615
|
-
.sort((left, right) => String(left.id).localeCompare(String(right.id))),
|
|
616
|
-
})),
|
|
617
|
-
claims: ['planning', 'operations', 'delivery'],
|
|
618
|
-
},
|
|
619
|
-
];
|
|
620
|
-
const items = [];
|
|
621
|
-
let truncated = false;
|
|
622
|
-
let remainingBytes = input.budgets.maxBytes;
|
|
623
|
-
for (const record of records) {
|
|
624
|
-
if (remainingBytes < 1_024 || items.length >= input.budgets.maxItems) {
|
|
625
|
-
truncated = true;
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
const bounded = boundedRecordsContent(record.id, record.values, Math.min(input.budgets.maxItemBytes, remainingBytes));
|
|
629
|
-
truncated ||= bounded.truncated || record.values.length >= maxRecords;
|
|
630
|
-
remainingBytes -= Buffer.byteLength(bounded.content);
|
|
631
|
-
if (bounded.records === 0)
|
|
632
|
-
continue;
|
|
633
|
-
const collectedAt = input.now.toISOString();
|
|
634
|
-
items.push({
|
|
635
|
-
id: evidenceId('linear', `${record.id}:${bounded.content}`),
|
|
636
|
-
source: 'linear',
|
|
637
|
-
location: record.id,
|
|
638
|
-
content: bounded.content,
|
|
639
|
-
collectedAt,
|
|
640
|
-
observedFrom: null,
|
|
641
|
-
observedTo: collectedAt,
|
|
642
|
-
freshness: 'fresh',
|
|
643
|
-
sensitivity: 'confidential',
|
|
644
|
-
claimTypes: record.claims,
|
|
645
|
-
quality: 'observed',
|
|
646
|
-
coverage: bounded.truncated ? 'partial' : 'complete',
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
return { items, truncated };
|
|
650
|
-
}
|
|
651
|
-
async function fileImportItems(projectRoot, roots, budgets, now, deadline, localRoot) {
|
|
652
|
-
const preferencesPath = path.join(resolveOperatingPaths(projectRoot, { localRoot }).localRoot, 'preferences.json');
|
|
653
|
-
const preferences = JSON.parse(await readFile(preferencesPath, 'utf8'));
|
|
654
|
-
const importPaths = Array.isArray(preferences.importPaths)
|
|
655
|
-
? preferences.importPaths.filter((entry) => typeof entry === 'string')
|
|
656
|
-
: [];
|
|
657
|
-
if (importPaths.length === 0) {
|
|
658
|
-
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'The file-import source has no configured evidence paths; rerun `planr operate init --evidence-file <path>`.');
|
|
659
|
-
}
|
|
660
|
-
const allowedRoots = Object.entries(roots.roots).map(([componentId, root]) => ({
|
|
661
|
-
componentId,
|
|
662
|
-
root,
|
|
663
|
-
}));
|
|
664
|
-
const items = [];
|
|
665
|
-
let bytes = 0;
|
|
666
|
-
let truncated = false;
|
|
667
|
-
for (const configuredPath of importPaths) {
|
|
668
|
-
if (Date.now() >= deadline ||
|
|
669
|
-
items.length >= budgets.maxItems ||
|
|
670
|
-
items.length >= budgets.maxFiles) {
|
|
671
|
-
truncated = true;
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
const remainingBytes = budgets.maxBytes - bytes;
|
|
675
|
-
if (remainingBytes < 1) {
|
|
676
|
-
truncated = true;
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
const imported = await readImportedEvidenceFile({
|
|
680
|
-
projectRoot,
|
|
681
|
-
configuredPath,
|
|
682
|
-
roots: allowedRoots,
|
|
683
|
-
maxBytes: Math.min(budgets.maxItemBytes, remainingBytes),
|
|
684
|
-
});
|
|
685
|
-
const commercialMetric = inferImportedCommercialMetric(imported.content, imported.format);
|
|
686
|
-
bytes += imported.byteCount;
|
|
687
|
-
const collectedAt = now.toISOString();
|
|
688
|
-
items.push({
|
|
689
|
-
id: evidenceId('file-import', `${imported.location}:${canonicalDigest(imported.content)}`),
|
|
690
|
-
source: 'file-import',
|
|
691
|
-
location: imported.location,
|
|
692
|
-
content: imported.content,
|
|
693
|
-
collectedAt,
|
|
694
|
-
observedFrom: null,
|
|
695
|
-
observedTo: collectedAt,
|
|
696
|
-
freshness: 'fresh',
|
|
697
|
-
sensitivity: 'internal',
|
|
698
|
-
claimTypes: [
|
|
699
|
-
'structured-data',
|
|
700
|
-
imported.format,
|
|
701
|
-
...(commercialMetric.commercial ? ['commercial-metric'] : []),
|
|
702
|
-
],
|
|
703
|
-
quality: 'self-reported',
|
|
704
|
-
coverage: 'complete',
|
|
705
|
-
...(commercialMetric.metric ? { metric: commercialMetric.metric } : {}),
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
return { items, truncated };
|
|
709
|
-
}
|
|
710
|
-
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;
|
|
711
|
-
const NUMERIC_CLAIM = /(?:[$€£]\s*)?\b\d+(?:[.,]\d+)?\s*(?:%|percent|usd|eur|gbp|customers?|users?)?\b/i;
|
|
712
|
-
function validIsoWindow(from, to) {
|
|
713
|
-
return (typeof from === 'string' &&
|
|
714
|
-
typeof to === 'string' &&
|
|
715
|
-
Number.isFinite(Date.parse(from)) &&
|
|
716
|
-
Number.isFinite(Date.parse(to)) &&
|
|
717
|
-
Date.parse(to) >= Date.parse(from));
|
|
718
|
-
}
|
|
719
|
-
function inferImportedCommercialMetric(content, format) {
|
|
720
|
-
const commercial = COMMERCIAL_METRIC_TERMS.test(content) && NUMERIC_CLAIM.test(content);
|
|
721
|
-
if (!commercial || format !== 'json')
|
|
722
|
-
return { commercial };
|
|
723
|
-
try {
|
|
724
|
-
const parsed = JSON.parse(content);
|
|
725
|
-
const candidate = parsed.metric && typeof parsed.metric === 'object'
|
|
726
|
-
? parsed.metric
|
|
727
|
-
: null;
|
|
728
|
-
if (!candidate ||
|
|
729
|
-
typeof candidate.identity !== 'string' ||
|
|
730
|
-
!candidate.identity.trim() ||
|
|
731
|
-
typeof candidate.query !== 'string' ||
|
|
732
|
-
!candidate.query.trim() ||
|
|
733
|
-
!validIsoWindow(candidate.observedFrom, candidate.observedTo)) {
|
|
734
|
-
return { commercial };
|
|
735
|
-
}
|
|
736
|
-
return {
|
|
737
|
-
commercial,
|
|
738
|
-
metric: {
|
|
739
|
-
identity: candidate.identity.trim(),
|
|
740
|
-
query: candidate.query.trim(),
|
|
741
|
-
observedFrom: candidate.observedFrom,
|
|
742
|
-
observedTo: candidate.observedTo,
|
|
743
|
-
},
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
catch {
|
|
747
|
-
return { commercial };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
function incrementalEvidencePath(input, key) {
|
|
751
|
-
return path.join(resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot }).evidence, 'incremental', `${key.slice('sha256:'.length)}.json`);
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* FR4: the board's identity is the hash of its event-chain genesis event. The
|
|
755
|
-
* incremental cache key incorporates it so a board re-inited at the same path —
|
|
756
|
-
* whose workspace components (remote/branch) are unchanged and would otherwise
|
|
757
|
-
* compute an identical key — never reuses a prior generation's baseline. Returns
|
|
758
|
-
* '' when no committed chain exists yet or it cannot be read (the collection then
|
|
759
|
-
* behaves exactly as before this change).
|
|
760
|
-
*/
|
|
761
|
-
async function evidenceBoardIdentity(input) {
|
|
762
|
-
try {
|
|
763
|
-
const { events } = await new OperatingEventStore(input.projectRoot, {
|
|
764
|
-
localRoot: input.localRoot,
|
|
765
|
-
}).replay();
|
|
766
|
-
const genesis = events.find((event) => event.previousEventHash === null) ?? events[0];
|
|
767
|
-
return genesis?.eventHash ?? '';
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
return '';
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
async function readIncrementalEvidence(input, key) {
|
|
774
|
-
if (!input.incremental)
|
|
775
|
-
return null;
|
|
776
|
-
try {
|
|
777
|
-
const record = JSON.parse(await readFile(incrementalEvidencePath(input, key), 'utf8'));
|
|
778
|
-
if (record.implementation !== 'openplanr-operate-incremental-evidence' || record.key !== key) {
|
|
779
|
-
return null;
|
|
780
|
-
}
|
|
781
|
-
// FR4: honor the baseline's own already-written workspaceDigest. A record
|
|
782
|
-
// whose captured workspace no longer matches the current one is treated as
|
|
783
|
-
// no-baseline so the cycle recollects deep, rather than trusting a snapshot
|
|
784
|
-
// bound to a superseded workspace (for example, a prior board generation).
|
|
785
|
-
if (record.workspaceDigest !== input.workspace.workspaceDigest) {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
await assertOperatingArtifact('operating-evidence', record.evidence);
|
|
789
|
-
return record;
|
|
790
|
-
}
|
|
791
|
-
catch {
|
|
792
|
-
return null;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
async function writeIncrementalEvidence(input, key, evidence) {
|
|
796
|
-
if (!input.persistIncremental ||
|
|
797
|
-
input.providers.some((provider) => ['file-import', 'github', 'linear'].includes(provider))) {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const target = incrementalEvidencePath(input, key);
|
|
801
|
-
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
802
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
803
|
-
const record = {
|
|
804
|
-
implementation: 'openplanr-operate-incremental-evidence',
|
|
805
|
-
key,
|
|
806
|
-
workspaceDigest: input.workspace.workspaceDigest,
|
|
807
|
-
evidence,
|
|
808
|
-
};
|
|
809
|
-
await writeFile(temporary, `${canonicalize(record)}\n`, { mode: 0o600 });
|
|
810
|
-
await rename(temporary, target);
|
|
811
|
-
}
|
|
812
|
-
function evidenceLocationIdentity(item) {
|
|
813
|
-
return `${item.source}\0${item.location}`;
|
|
814
|
-
}
|
|
815
|
-
function buildEvidenceDelta(input) {
|
|
816
|
-
const baselineItems = input.baseline?.items ?? [];
|
|
817
|
-
const baselineByLocation = new Map(baselineItems.map((item) => [evidenceLocationIdentity(item), item]));
|
|
818
|
-
const currentByLocation = new Map(input.current.map((item) => [evidenceLocationIdentity(item), item]));
|
|
819
|
-
const currentById = new Map(input.current.map((item) => [item.id, item]));
|
|
820
|
-
const baselineById = new Map(baselineItems.map((item) => [item.id, item]));
|
|
821
|
-
const changedEvidenceRefs = input.current
|
|
822
|
-
.filter((item) => {
|
|
823
|
-
const prior = baselineByLocation.get(evidenceLocationIdentity(item));
|
|
824
|
-
return (!prior ||
|
|
825
|
-
prior.digest !== item.digest ||
|
|
826
|
-
prior.sensitivity !== item.sensitivity ||
|
|
827
|
-
canonicalDigest(prior.metric ?? null) !== canonicalDigest(item.metric ?? null));
|
|
828
|
-
})
|
|
829
|
-
.map((item) => item.id)
|
|
830
|
-
.sort();
|
|
831
|
-
const removedEvidenceRefs = baselineItems
|
|
832
|
-
.filter((item) => !currentByLocation.has(evidenceLocationIdentity(item)))
|
|
833
|
-
.map((item) => item.id)
|
|
834
|
-
.sort();
|
|
835
|
-
const requiredEvidenceRefs = [
|
|
836
|
-
...new Set(input.requiredEvidenceRefs.flatMap((reference) => {
|
|
837
|
-
const current = currentById.get(reference);
|
|
838
|
-
if (current)
|
|
839
|
-
return [current.id];
|
|
840
|
-
const prior = baselineById.get(reference);
|
|
841
|
-
if (!prior)
|
|
842
|
-
return [];
|
|
843
|
-
return [currentByLocation.get(evidenceLocationIdentity(prior))?.id ?? prior.id];
|
|
844
|
-
})),
|
|
845
|
-
].sort();
|
|
846
|
-
return {
|
|
847
|
-
mode: input.incremental ? (input.baseline ? 'standard' : 'baseline') : 'deep',
|
|
848
|
-
baselineFingerprint: input.baseline?.fingerprint ?? null,
|
|
849
|
-
changedEvidenceRefs,
|
|
850
|
-
requiredEvidenceRefs,
|
|
851
|
-
selectedEvidenceRefs: [...new Set([...changedEvidenceRefs, ...requiredEvidenceRefs])].sort(),
|
|
852
|
-
removedEvidenceRefs,
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
export async function collectOperatingEvidence(input) {
|
|
856
|
-
const started = Date.now();
|
|
857
|
-
const now = input.now ?? new Date();
|
|
858
|
-
const deadline = started + input.budgets.maxDurationMs;
|
|
859
|
-
const incrementalKey = canonicalDigest({
|
|
860
|
-
boardIdentity: await evidenceBoardIdentity(input),
|
|
861
|
-
workspaceComponents: [input.workspace.controlRepository, ...input.workspace.components]
|
|
862
|
-
.map((component) => ({
|
|
863
|
-
componentId: component.componentId,
|
|
864
|
-
canonicalRemote: component.canonicalRemote,
|
|
865
|
-
configuredBranch: component.configuredBranch,
|
|
866
|
-
}))
|
|
867
|
-
.sort((left, right) => left.componentId.localeCompare(right.componentId)),
|
|
868
|
-
providers: [...new Set(input.providers)].sort(),
|
|
869
|
-
sensitivityCeiling: input.sensitivityCeiling,
|
|
870
|
-
budgets: input.budgets,
|
|
871
|
-
});
|
|
872
|
-
const baselineRecord = await readIncrementalEvidence(input, incrementalKey);
|
|
873
|
-
const roots = await readRoots(input.projectRoot, input.localRoot);
|
|
874
|
-
const raw = [];
|
|
875
|
-
const warnings = [];
|
|
876
|
-
let truncated = false;
|
|
877
|
-
if (input.providers.includes('repository') || input.providers.includes('planr')) {
|
|
878
|
-
const repository = await repositoryItems(roots, input.workspace, input.budgets, now, {
|
|
879
|
-
includeRepository: input.providers.includes('repository'),
|
|
880
|
-
includePlanr: input.providers.includes('planr'),
|
|
881
|
-
deadline,
|
|
882
|
-
});
|
|
883
|
-
raw.push(...repository.items);
|
|
884
|
-
truncated ||= repository.truncated;
|
|
885
|
-
}
|
|
886
|
-
if (input.providers.includes('git') && Date.now() < deadline) {
|
|
887
|
-
const git = await gitItems(roots, input.workspace, now, deadline);
|
|
888
|
-
raw.push(...git.items);
|
|
889
|
-
truncated ||= git.truncated;
|
|
890
|
-
}
|
|
891
|
-
if (input.providers.includes('github') && Date.now() < deadline) {
|
|
892
|
-
try {
|
|
893
|
-
const github = await collectGitHubEvidence({
|
|
894
|
-
projectRoot: input.projectRoot,
|
|
895
|
-
workspace: input.workspace,
|
|
896
|
-
now,
|
|
897
|
-
deadline,
|
|
898
|
-
budgets: input.budgets,
|
|
899
|
-
token: input.remote?.githubToken,
|
|
900
|
-
fetchImpl: input.remote?.fetchImpl,
|
|
901
|
-
});
|
|
902
|
-
raw.push(...github.items);
|
|
903
|
-
truncated ||= github.truncated;
|
|
904
|
-
if (github.items.length === 0) {
|
|
905
|
-
warnings.push('GitHub evidence found no supported repositories or records.');
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
catch {
|
|
909
|
-
warnings.push('GitHub evidence is unavailable or not authenticated.');
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
if (input.providers.includes('linear') && Date.now() < deadline) {
|
|
913
|
-
try {
|
|
914
|
-
const linear = await collectLinearEvidence({
|
|
915
|
-
projectRoot: input.projectRoot,
|
|
916
|
-
now,
|
|
917
|
-
deadline,
|
|
918
|
-
budgets: input.budgets,
|
|
919
|
-
token: input.remote?.linearToken,
|
|
920
|
-
fetchImpl: input.remote?.fetchImpl,
|
|
921
|
-
});
|
|
922
|
-
raw.push(...linear.items);
|
|
923
|
-
truncated ||= linear.truncated;
|
|
924
|
-
if (linear.items.length === 0) {
|
|
925
|
-
warnings.push('Linear evidence returned no configured teams, issues, or projects.');
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
catch {
|
|
929
|
-
warnings.push('Linear evidence is unavailable, not configured, or not authenticated.');
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (input.providers.includes('file-import') && Date.now() < deadline) {
|
|
933
|
-
const imported = await fileImportItems(input.projectRoot, roots, input.budgets, now, deadline, input.localRoot);
|
|
934
|
-
raw.push(...imported.items);
|
|
935
|
-
truncated ||= imported.truncated;
|
|
936
|
-
}
|
|
937
|
-
const rejected = raw.filter((item) => compareSensitivity(item.sensitivity, input.sensitivityCeiling) > 0);
|
|
938
|
-
if (rejected.length > 0) {
|
|
939
|
-
warnings.push(`${rejected.length} evidence item(s) exceeded the configured sensitivity ceiling.`);
|
|
940
|
-
}
|
|
941
|
-
const flattened = [];
|
|
942
|
-
for (const item of raw
|
|
943
|
-
.filter((candidate) => compareSensitivity(candidate.sensitivity, input.sensitivityCeiling) <= 0)
|
|
944
|
-
.slice(0, input.budgets.maxItems)) {
|
|
945
|
-
try {
|
|
946
|
-
flattened.push(sanitizeEvidenceItem(item));
|
|
947
|
-
}
|
|
948
|
-
catch (error) {
|
|
949
|
-
if (!(error instanceof OperateError) || error.code !== 'E_OPERATE_SECRET_DETECTED') {
|
|
950
|
-
throw error;
|
|
951
|
-
}
|
|
952
|
-
const details = error.details ?? {};
|
|
953
|
-
const fallback = detectSecretMetadata(item.content)[0];
|
|
954
|
-
const ruleId = typeof details.ruleId === 'string' ? details.ruleId : (fallback?.ruleId ?? 'unknown.v1');
|
|
955
|
-
const category = typeof details.category === 'string'
|
|
956
|
-
? details.category
|
|
957
|
-
: (fallback?.category ?? 'structured-secret');
|
|
958
|
-
const diagnostic = await createEvidenceDiagnostic({
|
|
959
|
-
projectRoot: input.projectRoot,
|
|
960
|
-
item,
|
|
961
|
-
localRoot: input.localRoot,
|
|
962
|
-
detection: {
|
|
963
|
-
ruleId,
|
|
964
|
-
category,
|
|
965
|
-
line: typeof details.line === 'number' && Number.isInteger(details.line)
|
|
966
|
-
? details.line
|
|
967
|
-
: (fallback?.line ?? 1),
|
|
968
|
-
hardBlock: typeof details.hardBlock === 'boolean'
|
|
969
|
-
? details.hardBlock
|
|
970
|
-
: (fallback?.hardBlock ?? false),
|
|
971
|
-
},
|
|
972
|
-
});
|
|
973
|
-
if (diagnostic.classification?.status === 'false-positive') {
|
|
974
|
-
warnings.push(`Evidence candidate ${diagnostic.candidateId} was omitted under its exact false-positive classification.`);
|
|
975
|
-
continue;
|
|
976
|
-
}
|
|
977
|
-
// Quarantine the item instead of aborting the entire evidence snapshot.
|
|
978
|
-
// The immutable role-filtered packs never receive its bytes. Readiness
|
|
979
|
-
// evaluation below decides whether enough eligible evidence remains for
|
|
980
|
-
// each lens; an affected lens becomes not_evaluated with a governed gap
|
|
981
|
-
// rather than preventing unrelated lenses from producing useful work.
|
|
982
|
-
warnings.push([
|
|
983
|
-
`Evidence candidate ${diagnostic.candidateId} was quarantined`,
|
|
984
|
-
`(${diagnostic.category}); its value was not persisted or dispatched.`,
|
|
985
|
-
`Inspect with: planr operate evidence diagnose ${diagnostic.candidateId} --json`,
|
|
986
|
-
].join(' '));
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
if (Date.now() >= deadline) {
|
|
990
|
-
truncated = true;
|
|
991
|
-
warnings.push('Evidence collection reached the configured duration budget.');
|
|
992
|
-
}
|
|
993
|
-
const sourceIds = [...new Set(input.providers)].sort();
|
|
994
|
-
const delta = buildEvidenceDelta({
|
|
995
|
-
incremental: Boolean(input.incremental),
|
|
996
|
-
current: flattened,
|
|
997
|
-
baseline: baselineRecord?.evidence ?? null,
|
|
998
|
-
requiredEvidenceRefs: input.requiredEvidenceRefs ?? [],
|
|
999
|
-
});
|
|
1000
|
-
const baselineSources = new Map((baselineRecord?.evidence.sources ?? []).map((source) => [source.id, source]));
|
|
1001
|
-
const sources = sourceIds.map((id) => {
|
|
1002
|
-
const items = flattened.filter((item) => item.source === id);
|
|
1003
|
-
const fingerprint = canonicalDigest(evidenceFingerprintItems(items));
|
|
1004
|
-
const prior = baselineSources.get(id);
|
|
1005
|
-
return {
|
|
1006
|
-
id,
|
|
1007
|
-
fingerprint,
|
|
1008
|
-
status: items.length === 0
|
|
1009
|
-
? 'unavailable'
|
|
1010
|
-
: delta.mode === 'standard' && prior?.fingerprint === fingerprint
|
|
1011
|
-
? 'unchanged'
|
|
1012
|
-
: delta.mode === 'standard'
|
|
1013
|
-
? 'partial'
|
|
1014
|
-
: 'collected',
|
|
1015
|
-
itemCount: items.length,
|
|
1016
|
-
byteCount: items.reduce((sum, item) => sum + Buffer.byteLength(item.summary ?? ''), 0),
|
|
1017
|
-
};
|
|
1018
|
-
});
|
|
1019
|
-
if (delta.mode === 'standard') {
|
|
1020
|
-
warnings.push(`Standard evidence delta selected ${delta.changedEvidenceRefs.length} changed and ${delta.requiredEvidenceRefs.length} open-item-required evidence item(s).`);
|
|
1021
|
-
}
|
|
1022
|
-
const evidence = {
|
|
1023
|
-
kind: 'operating-evidence',
|
|
1024
|
-
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
1025
|
-
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
1026
|
-
cycleId: input.cycleId,
|
|
1027
|
-
fingerprint: canonicalDigest({
|
|
1028
|
-
workspaceDigest: input.workspace.workspaceDigest,
|
|
1029
|
-
items: evidenceFingerprintItems(flattened),
|
|
1030
|
-
sources,
|
|
1031
|
-
delta,
|
|
1032
|
-
}),
|
|
1033
|
-
collectedAt: now.toISOString(),
|
|
1034
|
-
truncated: truncated ||
|
|
1035
|
-
flattened.length >= input.budgets.maxItems ||
|
|
1036
|
-
sources.reduce((sum, source) => sum + source.byteCount, 0) >= input.budgets.maxBytes,
|
|
1037
|
-
items: flattened,
|
|
1038
|
-
delta,
|
|
1039
|
-
sources,
|
|
1040
|
-
warnings,
|
|
1041
|
-
};
|
|
1042
|
-
const validated = await assertOperatingArtifact('operating-evidence', evidence);
|
|
1043
|
-
await writeIncrementalEvidence(input, incrementalKey, validated);
|
|
1044
|
-
return validated;
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Convert commercial numbers without reproducible query/window identity into
|
|
1048
|
-
* governed gaps before any advisor can turn them into recommendations.
|
|
1049
|
-
*/
|
|
1050
|
-
export async function unqualifiedCommercialEvidenceGaps(input) {
|
|
1051
|
-
const unqualified = input.evidence.items
|
|
1052
|
-
.filter((item) => item.claimTypes.includes('commercial-metric') &&
|
|
1053
|
-
(!item.metric?.identity.trim() ||
|
|
1054
|
-
!item.metric.query.trim() ||
|
|
1055
|
-
!validIsoWindow(item.metric.observedFrom, item.metric.observedTo)))
|
|
1056
|
-
.sort((left, right) => left.source.localeCompare(right.source) ||
|
|
1057
|
-
left.location.localeCompare(right.location) ||
|
|
1058
|
-
left.id.localeCompare(right.id));
|
|
1059
|
-
const gaps = unqualified.map((item, index) => ({
|
|
1060
|
-
kind: 'operating-data-gap',
|
|
1061
|
-
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
1062
|
-
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
1063
|
-
id: `GAP-${String(index + 1).padStart(3, '0')}`,
|
|
1064
|
-
cycleId: input.cycleId,
|
|
1065
|
-
question: `What reproducible metric query and observation window qualify ${item.location}?`,
|
|
1066
|
-
reason: 'Numeric commercial evidence requires a metric identity, query identity, and bounded observation window before it can support an operating recommendation.',
|
|
1067
|
-
unblocks: [],
|
|
1068
|
-
affectedRoles: [
|
|
1069
|
-
'strategy-finance',
|
|
1070
|
-
'product-activation',
|
|
1071
|
-
'growth-market',
|
|
1072
|
-
'operations-customer',
|
|
1073
|
-
],
|
|
1074
|
-
status: 'open',
|
|
1075
|
-
owner: input.owner,
|
|
1076
|
-
evidenceRefs: [item.id],
|
|
1077
|
-
createdAt: input.now,
|
|
1078
|
-
updatedAt: input.now,
|
|
1079
|
-
}));
|
|
1080
|
-
await Promise.all(gaps.map((gap) => assertOperatingArtifact('operating-data-gap', gap)));
|
|
1081
|
-
return gaps;
|
|
1082
|
-
}
|
|
1083
8
|
export function evidenceProjectionSources(evidence) {
|
|
1084
9
|
return evidence.sources.map((source) => ({
|
|
1085
10
|
id: source.id,
|
|
@@ -1092,123 +17,4 @@ export function evidenceProjectionSources(evidence) {
|
|
|
1092
17
|
itemCount: source.itemCount,
|
|
1093
18
|
}));
|
|
1094
19
|
}
|
|
1095
|
-
// The Protocol v1.3 index-item schema pins these patterns; every index item we
|
|
1096
|
-
// emit must satisfy them or the pipeline's mission-packet validator fails
|
|
1097
|
-
// closed. Building here — rather than trusting the caller — keeps a malformed
|
|
1098
|
-
// path or revision out of a dispatched packet.
|
|
1099
|
-
const INDEX_PATH_PATTERN = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
1100
|
-
const INDEX_REVISION_PATTERN = /^[a-f0-9]{7,64}$/;
|
|
1101
|
-
const INDEX_SOURCE_CLASSIFICATION = {
|
|
1102
|
-
repository: 'source-code',
|
|
1103
|
-
planr: 'planning-artifact',
|
|
1104
|
-
git: 'change-history',
|
|
1105
|
-
'file-import': 'imported-record',
|
|
1106
|
-
github: 'integration-record',
|
|
1107
|
-
linear: 'integration-record',
|
|
1108
|
-
};
|
|
1109
|
-
/**
|
|
1110
|
-
* Derive a stable, schema-conformant index-item id (`EVX-…`) from the source
|
|
1111
|
-
* evidence id. The evidence ids are unique, so the derived ids are too.
|
|
1112
|
-
*/
|
|
1113
|
-
function evidenceIndexItemId(evidenceId) {
|
|
1114
|
-
return `EVX-${canonicalDigest(evidenceId).slice('sha256:'.length, 27)}`;
|
|
1115
|
-
}
|
|
1116
|
-
/**
|
|
1117
|
-
* Strip the leading component id from an evidence `location`
|
|
1118
|
-
* (`<componentId>/<relativePath>`) to recover the repository-relative path, or
|
|
1119
|
-
* return null when the result cannot be a schema-valid index path (for example
|
|
1120
|
-
* dot-prefixed `.planr/…` paths, which the pattern forbids).
|
|
1121
|
-
*/
|
|
1122
|
-
function evidenceIndexPath(location) {
|
|
1123
|
-
const relative = location.split('/').slice(1).join('/');
|
|
1124
|
-
return relative && INDEX_PATH_PATTERN.test(relative) ? relative : null;
|
|
1125
|
-
}
|
|
1126
|
-
function evidenceIndexSignals(item) {
|
|
1127
|
-
const signals = new Set();
|
|
1128
|
-
for (const claim of item.claimTypes) {
|
|
1129
|
-
const trimmed = claim.trim();
|
|
1130
|
-
if (trimmed)
|
|
1131
|
-
signals.add(trimmed.slice(0, 256));
|
|
1132
|
-
}
|
|
1133
|
-
if (item.freshness === 'stale')
|
|
1134
|
-
signals.add('stale-evidence');
|
|
1135
|
-
if (item.repository?.dirtyFingerprint)
|
|
1136
|
-
signals.add('uncommitted-working-tree');
|
|
1137
|
-
return [...signals].sort();
|
|
1138
|
-
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Build Protocol v1.3 evidence index items (`operating-evidence-index-item@1.3.0`)
|
|
1141
|
-
* from a collected evidence snapshot for mission-mode dispatch. Each item is a
|
|
1142
|
-
* pointer only — path OR revision, content hash, source, classification,
|
|
1143
|
-
* freshness, sensitivity, and detected signals — and carries NO file body. Git
|
|
1144
|
-
* evidence is referenced by its pinned revision; repository/planr/file-import
|
|
1145
|
-
* evidence by its repository-relative path. Items that cannot be represented as
|
|
1146
|
-
* a schema-valid path or revision pointer (github/linear records, dot-prefixed
|
|
1147
|
-
* planr paths) are omitted from the index rather than emitted malformed.
|
|
1148
|
-
*/
|
|
1149
|
-
export function buildOperatingEvidenceIndex(evidence, options = {}) {
|
|
1150
|
-
const seen = new Set();
|
|
1151
|
-
const items = [];
|
|
1152
|
-
for (const item of evidence.items) {
|
|
1153
|
-
if (options.sensitivityCeiling &&
|
|
1154
|
-
compareSensitivity(item.sensitivity, options.sensitivityCeiling) > 0) {
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
const source = item.source;
|
|
1158
|
-
const classification = INDEX_SOURCE_CLASSIFICATION[source];
|
|
1159
|
-
if (!classification)
|
|
1160
|
-
continue;
|
|
1161
|
-
const base = {
|
|
1162
|
-
id: evidenceIndexItemId(item.id),
|
|
1163
|
-
contentHash: item.digest,
|
|
1164
|
-
source,
|
|
1165
|
-
classification,
|
|
1166
|
-
freshness: item.freshness,
|
|
1167
|
-
sensitivity: item.sensitivity,
|
|
1168
|
-
signals: evidenceIndexSignals(item),
|
|
1169
|
-
};
|
|
1170
|
-
if (seen.has(base.id))
|
|
1171
|
-
continue;
|
|
1172
|
-
if (source === 'git') {
|
|
1173
|
-
const revision = item.repository?.revision;
|
|
1174
|
-
if (revision && INDEX_REVISION_PATTERN.test(revision)) {
|
|
1175
|
-
seen.add(base.id);
|
|
1176
|
-
items.push({ ...base, revision });
|
|
1177
|
-
}
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
if (source === 'repository' || source === 'planr' || source === 'file-import') {
|
|
1181
|
-
const relativePath = evidenceIndexPath(item.location);
|
|
1182
|
-
if (relativePath) {
|
|
1183
|
-
seen.add(base.id);
|
|
1184
|
-
items.push({ ...base, path: relativePath });
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
return items.sort((left, right) => {
|
|
1189
|
-
if (left.source !== right.source)
|
|
1190
|
-
return left.source < right.source ? -1 : 1;
|
|
1191
|
-
const leftRef = left.path ?? left.revision ?? '';
|
|
1192
|
-
const rightRef = right.path ?? right.revision ?? '';
|
|
1193
|
-
if (leftRef !== rightRef)
|
|
1194
|
-
return leftRef < rightRef ? -1 : 1;
|
|
1195
|
-
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* The read roots a mission agent may traverse, derived from the top-level
|
|
1200
|
-
* directory segment of every path-referenced index item. Schema-valid and
|
|
1201
|
-
* deduplicated; empty when the index carries only revision pointers.
|
|
1202
|
-
*/
|
|
1203
|
-
export function deriveOperatingDeclaredRoots(index) {
|
|
1204
|
-
const roots = new Set();
|
|
1205
|
-
for (const item of index) {
|
|
1206
|
-
if (!item.path)
|
|
1207
|
-
continue;
|
|
1208
|
-
const top = item.path.split('/')[0];
|
|
1209
|
-
if (top && INDEX_PATH_PATTERN.test(top))
|
|
1210
|
-
roots.add(top);
|
|
1211
|
-
}
|
|
1212
|
-
return [...roots].sort();
|
|
1213
|
-
}
|
|
1214
20
|
//# sourceMappingURL=evidence.js.map
|