wendkeep 0.88.0 → 0.89.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/CHANGELOG.md +17 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/bin/wendkeep.mjs +1 -0
- package/docs/en/commands/ecosystem-bridges.md +172 -0
- package/docs/en/commands/verify.md +6 -0
- package/docs/pt-BR/commands/ecosystem-bridges.md +169 -0
- package/docs/pt-BR/commands/verify.md +6 -0
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +10 -1
- package/packages/harness/src/sensors-core.mjs +49 -3
- package/packages/integrations/src/bridge-config.mjs +139 -0
- package/packages/integrations/src/bridge-contract.mjs +316 -0
- package/packages/integrations/src/bridge-diagnostics.mjs +45 -0
- package/packages/integrations/src/canonical-bridge-authority.mjs +32 -0
- package/packages/integrations/src/capabilities.mjs +34 -0
- package/packages/integrations/src/ecosystem-bridge.mjs +82 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/spec-kit-adapter.mjs +259 -0
- package/packages/integrations/src/superpowers-adapter.mjs +269 -0
- package/schema/ecosystem-bridge-artifact-manifest-v1.schema.json +30 -0
- package/schema/ecosystem-bridge-v1.schema.json +65 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +39 -0
- package/schema/wendkeep.sensors.schema.json +14 -0
- package/src/doctor.mjs +6 -1
- package/src/ecosystem-bridge-artifact-collector.mjs +111 -0
- package/src/ecosystem-bridge-baseline.mjs +58 -0
- package/src/ecosystem-bridge-proof.mjs +97 -0
- package/src/ecosystem-bridges.mjs +227 -0
- package/src/evidence-envelope.mjs +2 -0
- package/src/task-contracts.mjs +19 -0
- package/src/task.mjs +82 -0
- package/src/verify.mjs +9 -0
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"type": "array",
|
|
53
53
|
"items": { "$ref": "#/$defs/sensor" }
|
|
54
54
|
},
|
|
55
|
+
"external_artifacts": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "$ref": "#/$defs/externalArtifact" }
|
|
58
|
+
},
|
|
55
59
|
"host_coverage": { "$ref": "host-coverage-v1.schema.json" },
|
|
56
60
|
"tdd_attestations": {
|
|
57
61
|
"type": "array",
|
|
@@ -100,10 +104,45 @@
|
|
|
100
104
|
"exit_code": { "type": ["integer", "null"] },
|
|
101
105
|
"output_sha256": { "$ref": "#/$defs/sha256" },
|
|
102
106
|
"output_tail": { "type": "string", "maxLength": 2000 },
|
|
107
|
+
"artifact_results": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": { "$ref": "#/$defs/sensorArtifactResult" }
|
|
110
|
+
},
|
|
103
111
|
"note": { "type": "string", "maxLength": 2000 },
|
|
104
112
|
"survivors": { "type": "array" },
|
|
105
113
|
"ts": { "type": "string", "format": "date-time" }
|
|
106
114
|
}
|
|
115
|
+
},
|
|
116
|
+
"sensorArtifactResult": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["schema_version", "external_id", "path", "algorithm", "digest"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"schema_version": { "const": 1 },
|
|
122
|
+
"external_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
123
|
+
"path": { "type": "string", "minLength": 1 },
|
|
124
|
+
"algorithm": { "const": "sha256" },
|
|
125
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"externalArtifact": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"required": ["schema_version", "source", "external_id", "kind", "path", "sha256", "authority", "sensor_id", "task_id", "git_blob", "manifest_path", "manifest_git_blob"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"schema_version": { "const": 1 },
|
|
134
|
+
"source": { "const": "superpowers" },
|
|
135
|
+
"external_id": { "type": "string", "minLength": 1 },
|
|
136
|
+
"kind": { "enum": ["artifact", "review", "commit"] },
|
|
137
|
+
"path": { "type": "string", "minLength": 1 },
|
|
138
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
139
|
+
"authority": { "enum": ["reported", "verified"] },
|
|
140
|
+
"sensor_id": { "type": "string", "minLength": 1 },
|
|
141
|
+
"task_id": { "type": "string", "minLength": 1 },
|
|
142
|
+
"git_blob": { "$ref": "#/$defs/gitObject" },
|
|
143
|
+
"manifest_path": { "const": ".wendkeep/bridge-artifacts.json" },
|
|
144
|
+
"manifest_git_blob": { "$ref": "#/$defs/gitObject" }
|
|
145
|
+
}
|
|
107
146
|
}
|
|
108
147
|
}
|
|
109
148
|
}
|
|
@@ -40,6 +40,20 @@
|
|
|
40
40
|
"severity": { "enum": ["critical", "warning"], "default": "critical" },
|
|
41
41
|
"type": { "enum": ["command", "mutation"], "default": "command" },
|
|
42
42
|
"command": { "type": "string", "minLength": 1 },
|
|
43
|
+
"artifact_results": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["schema_version", "external_id", "path", "algorithm"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"schema_version": { "const": 1 },
|
|
51
|
+
"external_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
52
|
+
"path": { "type": "string", "minLength": 1 },
|
|
53
|
+
"algorithm": { "const": "sha256" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
43
57
|
"report": {
|
|
44
58
|
"type": "string",
|
|
45
59
|
"description": "type: mutation only — path (project-relative) to the mutation-testing-elements report."
|
package/src/doctor.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `wendkeep doctor` — vault/session integrity (hooks/vault-health.mjs) PLUS the a2
|
|
2
2
|
// harness integrity check (hooks/harness-doctor.mjs). Exits 1 on any error.
|
|
3
|
+
import * as bridgeFs from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
|
|
5
6
|
import { diagnoseManagedWorktrees } from './worktree.mjs';
|
|
@@ -20,6 +21,7 @@ import { inspectPortableState } from './portable.mjs';
|
|
|
20
21
|
import { inspectSyncOutbox, readLocalSyncState } from './sync-outbox.mjs';
|
|
21
22
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
22
23
|
import { inspectGitCommitHooks } from './git-commit-hooks.mjs';
|
|
24
|
+
import { inspectEcosystemBridges, renderEcosystemBridgeLines } from '../packages/integrations/src/ecosystem-bridge.mjs';
|
|
23
25
|
|
|
24
26
|
const healthStatusLabel = (status) => ({
|
|
25
27
|
healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
|
|
@@ -178,6 +180,8 @@ export function runDoctor(argv) {
|
|
|
178
180
|
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
179
181
|
|
|
180
182
|
const worktrees = diagnoseManagedWorktrees({ startDir: projectRoot });
|
|
183
|
+
const bridges = inspectEcosystemBridges({ projectRoot, fs: bridgeFs });
|
|
184
|
+
process.stdout.write(`\n${renderEcosystemBridgeLines(bridges).join('\n')}\n`);
|
|
181
185
|
process.stdout.write(`\n[worktrees] ${worktrees.initialized ? `${worktrees.issues.length} problema(s)` : 'não inicializado'}\n`);
|
|
182
186
|
for (const issue of worktrees.issues) {
|
|
183
187
|
process.stdout.write(` → ${issue.slug}: ${issue.errorCode} — ${issue.repair}\n`);
|
|
@@ -264,6 +268,7 @@ export function runDoctor(argv) {
|
|
|
264
268
|
|| warnings.length
|
|
265
269
|
|| worktrees.issues.length
|
|
266
270
|
|| (commitHooks.configured && commitHooks.status !== 'healthy')
|
|
271
|
+
|| !bridges.ok
|
|
267
272
|
|| activeContexts.issues.length
|
|
268
273
|
|| ['diverged', 'invalid'].includes(portable.status)
|
|
269
274
|
|| sync.status === 'corrupt'
|
|
@@ -276,5 +281,5 @@ export function runDoctor(argv) {
|
|
|
276
281
|
|| (staleDerived.notes || staleDerived.items || []).length
|
|
277
282
|
|| !observability.ok
|
|
278
283
|
);
|
|
279
|
-
return (scope !== 'runtime' && (healthStatus !== 0 || recallStatus !== 0)) || errors.length || strictDebt ? 1 : 0;
|
|
284
|
+
return (scope !== 'runtime' && (healthStatus !== 0 || recallStatus !== 0)) || errors.length || !bridges.ok || strictDebt ? 1 : 0;
|
|
280
285
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { isProjectContainedPath } from '../packages/integrations/src/bridge-config.mjs';
|
|
7
|
+
|
|
8
|
+
const MANIFEST_PATH = '.wendkeep/bridge-artifacts.json';
|
|
9
|
+
const MAX_ARTIFACT_BYTES = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function fail(code, message) {
|
|
12
|
+
throw Object.assign(new Error(message), { code });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function gitText(projectRoot, args, spawn = spawnSync) {
|
|
16
|
+
const result = spawn('git', args, {
|
|
17
|
+
cwd: projectRoot, encoding: 'utf8', windowsHide: true,
|
|
18
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
19
|
+
});
|
|
20
|
+
return result.status === 0 && !result.error ? String(result.stdout || '').trim() : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function trackedBlob(projectRoot, path, spawn) {
|
|
24
|
+
if (!gitText(projectRoot, ['ls-files', '--error-unmatch', '--', path], spawn)) return '';
|
|
25
|
+
const working = gitText(projectRoot, ['hash-object', '--', path], spawn);
|
|
26
|
+
const indexed = gitText(projectRoot, ['rev-parse', `:${path}`], spawn);
|
|
27
|
+
return working && working === indexed ? indexed : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeProjectFile(projectRoot, path, code) {
|
|
31
|
+
const candidate = resolve(projectRoot, String(path || ''));
|
|
32
|
+
if (!String(path || '').trim() || !isProjectContainedPath(projectRoot, candidate)
|
|
33
|
+
|| !existsSync(candidate) || !lstatSync(candidate).isFile()) {
|
|
34
|
+
fail(code, `bridge artifact path is unavailable: ${path || '<missing>'}`);
|
|
35
|
+
}
|
|
36
|
+
const file = realpathSync(candidate);
|
|
37
|
+
if (!isProjectContainedPath(projectRoot, file) || lstatSync(file).size > MAX_ARTIFACT_BYTES) {
|
|
38
|
+
fail(code, `bridge artifact path is unsafe: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
return { file, path: relative(projectRoot, file).replaceAll('\\', '/') };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readManifest(projectRoot, spawn) {
|
|
44
|
+
const candidate = resolve(projectRoot, MANIFEST_PATH);
|
|
45
|
+
const tracked = gitText(projectRoot, ['ls-files', '--error-unmatch', '--', MANIFEST_PATH], spawn);
|
|
46
|
+
if (!tracked && !existsSync(candidate)) return null;
|
|
47
|
+
if (!tracked || !existsSync(candidate)) {
|
|
48
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_UNTRACKED', 'bridge artifact manifest must exist and match the Git index');
|
|
49
|
+
}
|
|
50
|
+
const manifest = safeProjectFile(projectRoot, MANIFEST_PATH, 'BRIDGE_ARTIFACT_MANIFEST_INVALID');
|
|
51
|
+
const gitBlob = trackedBlob(projectRoot, manifest.path, spawn);
|
|
52
|
+
if (!gitBlob) fail('BRIDGE_ARTIFACT_MANIFEST_UNTRACKED', 'bridge artifact manifest must match the Git index');
|
|
53
|
+
let parsed;
|
|
54
|
+
try { parsed = JSON.parse(readFileSync(manifest.file, 'utf8')); }
|
|
55
|
+
catch { fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest must be valid JSON'); }
|
|
56
|
+
if (parsed?.schema_version !== 1 || !Array.isArray(parsed?.artifacts)) {
|
|
57
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest requires schema_version 1 and artifacts');
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(parsed).some((key) => !['schema_version', 'artifacts'].includes(key))) {
|
|
60
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest contains unsupported fields');
|
|
61
|
+
}
|
|
62
|
+
return { artifacts: parsed.artifacts, gitBlob, path: manifest.path };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function collectBridgeArtifactEvidence({
|
|
66
|
+
projectRoot, tasks = [], sensors = [], spawn = spawnSync,
|
|
67
|
+
} = {}) {
|
|
68
|
+
const root = realpathSync(resolve(projectRoot));
|
|
69
|
+
const manifest = readManifest(root, spawn);
|
|
70
|
+
if (!manifest) return [];
|
|
71
|
+
const taskById = new Map((tasks || []).map((task) => [String(task.id || ''), task]));
|
|
72
|
+
const sensorById = new Map((sensors || []).map((sensor) => [String(sensor.id || ''), sensor]));
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
return manifest.artifacts.map((entry) => {
|
|
75
|
+
const externalId = String(entry?.external_id || '').trim();
|
|
76
|
+
const kind = String(entry?.kind || '').trim();
|
|
77
|
+
const sensorId = String(entry?.sensor_id || '').trim();
|
|
78
|
+
const taskId = String(entry?.task_id || '').trim();
|
|
79
|
+
if (Object.keys(entry || {}).some((key) => ![
|
|
80
|
+
'source', 'external_id', 'kind', 'path', 'sensor_id', 'task_id',
|
|
81
|
+
].includes(key)) || entry?.source !== 'superpowers' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(externalId)
|
|
82
|
+
|| !['artifact', 'review', 'commit'].includes(kind) || !sensorId || !taskId || seen.has(externalId)) {
|
|
83
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', `invalid or duplicate bridge artifact: ${externalId || '<missing>'}`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(externalId);
|
|
86
|
+
const task = taskById.get(taskId);
|
|
87
|
+
const requiredSensors = Array.isArray(task?.sensors) && task.sensors.length ? task.sensors : [task?.sensor].filter(Boolean);
|
|
88
|
+
if (!task || !requiredSensors.includes(sensorId)) {
|
|
89
|
+
fail('BRIDGE_ARTIFACT_TASK_UNBOUND', `bridge artifact ${externalId} is not bound to task ${taskId} and sensor ${sensorId}`);
|
|
90
|
+
}
|
|
91
|
+
const artifact = safeProjectFile(root, entry.path, 'BRIDGE_ARTIFACT_FORGED');
|
|
92
|
+
const gitBlob = trackedBlob(root, artifact.path, spawn);
|
|
93
|
+
if (!gitBlob) fail('BRIDGE_ARTIFACT_FORGED', `bridge artifact ${externalId} must match the Git index`);
|
|
94
|
+
const digest = createHash('sha256').update(readFileSync(artifact.file)).digest('hex');
|
|
95
|
+
const sensor = sensorById.get(sensorId);
|
|
96
|
+
const result = (sensor?.artifact_results || []).find((item) => (
|
|
97
|
+
item?.schema_version === 1 && item.external_id === externalId && item.path === artifact.path
|
|
98
|
+
&& item.algorithm === 'sha256' && item.digest === digest
|
|
99
|
+
));
|
|
100
|
+
if (sensor?.status === 'green' && sensor?.exit_code === 0 && !result) {
|
|
101
|
+
fail('BRIDGE_ARTIFACT_RESULT_MISSING', `green sensor ${sensorId} did not produce the bound digest for ${externalId}`);
|
|
102
|
+
}
|
|
103
|
+
const verified = sensor?.status === 'green' && sensor?.exit_code === 0 && Boolean(result);
|
|
104
|
+
return {
|
|
105
|
+
schema_version: 1,
|
|
106
|
+
source: 'superpowers', external_id: externalId, kind, path: artifact.path,
|
|
107
|
+
sha256: digest, authority: verified ? 'verified' : 'reported', sensor_id: sensorId, task_id: taskId,
|
|
108
|
+
git_blob: gitBlob, manifest_path: manifest.path, manifest_git_blob: manifest.gitBlob,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
5
|
+
import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
|
|
6
|
+
import { validateBridgeProjection } from '../packages/integrations/src/bridge-contract.mjs';
|
|
7
|
+
|
|
8
|
+
const BASELINE_FILE = 'spec-kit-baseline.v1.json';
|
|
9
|
+
|
|
10
|
+
function safeChangeSlug(value) {
|
|
11
|
+
const slug = String(value || '').trim();
|
|
12
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(slug) || slug.includes('..')) {
|
|
13
|
+
throw Object.assign(new Error('a safe change slug is required for the Spec Kit baseline'), {
|
|
14
|
+
code: 'BRIDGE_BASELINE_CONTEXT_INVALID',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return slug;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function specKitBaselinePath(vaultBase, changeSlug) {
|
|
21
|
+
return join(vaultBase, getLocale(vaultBase).folders.changes, safeChangeSlug(changeSlug), BASELINE_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readSpecKitBaseline(vaultBase, changeSlug) {
|
|
25
|
+
const path = specKitBaselinePath(vaultBase, changeSlug);
|
|
26
|
+
if (!existsSync(path)) return null;
|
|
27
|
+
let projection;
|
|
28
|
+
try { projection = JSON.parse(readFileSync(path, 'utf8')); } catch {
|
|
29
|
+
throw Object.assign(new Error('canonical Spec Kit baseline is invalid JSON'), { code: 'BRIDGE_BASELINE_INVALID' });
|
|
30
|
+
}
|
|
31
|
+
const validation = validateBridgeProjection(projection);
|
|
32
|
+
if (!validation.valid || projection.ok !== true || projection.adapter !== 'spec-kit') {
|
|
33
|
+
throw Object.assign(new Error('canonical Spec Kit baseline is invalid or blocked'), {
|
|
34
|
+
code: 'BRIDGE_BASELINE_INVALID', diagnostics: validation.diagnostics,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return projection;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function writeSpecKitBaseline(vaultBase, changeSlug, projection) {
|
|
41
|
+
const validation = validateBridgeProjection(projection);
|
|
42
|
+
if (!validation.valid || projection.ok !== true || projection.adapter !== 'spec-kit') {
|
|
43
|
+
throw Object.assign(new Error('only a valid green Spec Kit projection can become baseline'), {
|
|
44
|
+
code: 'BRIDGE_BASELINE_INVALID', diagnostics: validation.diagnostics,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const path = specKitBaselinePath(vaultBase, changeSlug);
|
|
48
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, safeChangeSlug(changeSlug));
|
|
49
|
+
if (!existsSync(changeDir)) {
|
|
50
|
+
throw Object.assign(new Error(`change not found for Spec Kit baseline: ${changeSlug}`), {
|
|
51
|
+
code: 'BRIDGE_BASELINE_CONTEXT_INVALID',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
writeVaultFileAtomic(vaultBase, path, `${JSON.stringify(projection, null, 2)}\n`, 'utf8', {
|
|
55
|
+
scopeRoot: changeDir, label: 'baseline canônico Spec Kit', code: 'BRIDGE_BASELINE_PATH_UNSAFE',
|
|
56
|
+
});
|
|
57
|
+
return projection;
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
6
|
+
import { parseTasks } from '../hooks/change-core.mjs';
|
|
7
|
+
import { buildEffectiveRequirementPackage, tasksHashOf } from '../hooks/spec-core.mjs';
|
|
8
|
+
import { loadSensorsDetailed, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
9
|
+
import { evaluateEvidenceBinding } from '../packages/vault/src/evidence-envelope.mjs';
|
|
10
|
+
import { bridgeSha256, canonicalBridgeJson } from '../packages/integrations/src/index.mjs';
|
|
11
|
+
import { issueCanonicalArtifactProof } from '../packages/integrations/src/canonical-bridge-authority.mjs';
|
|
12
|
+
import { captureGitSnapshot, resolveEvidenceIdentity, sensorConfigSha256 } from './evidence-envelope.mjs';
|
|
13
|
+
import { collectBridgeArtifactEvidence } from './ecosystem-bridge-artifact-collector.mjs';
|
|
14
|
+
|
|
15
|
+
function safeSlug(value) {
|
|
16
|
+
const slug = String(value || '').trim();
|
|
17
|
+
return /^[a-z0-9][a-z0-9._-]*$/i.test(slug) && !slug.includes('..') ? slug : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveCanonicalArtifactProof({
|
|
21
|
+
artifact, proof, projectRoot, vaultBase, changeSlug, sessionId = '', spawn = spawnSync,
|
|
22
|
+
} = {}) {
|
|
23
|
+
try {
|
|
24
|
+
const slug = safeSlug(changeSlug);
|
|
25
|
+
if (!slug || proof?.type !== 'evidence-envelope'
|
|
26
|
+
|| String(proof?.external_id || '') !== String(artifact?.external_id || '')) return null;
|
|
27
|
+
|
|
28
|
+
const root = realpathSync(resolve(projectRoot));
|
|
29
|
+
|
|
30
|
+
const evidencePath = join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'evidencia.json');
|
|
31
|
+
if (!existsSync(evidencePath) || !lstatSync(evidencePath).isFile()) return null;
|
|
32
|
+
const evidence = JSON.parse(readFileSync(evidencePath, 'utf8'));
|
|
33
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
34
|
+
const tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8');
|
|
35
|
+
const tasks = parseTasks(tarefas);
|
|
36
|
+
const sensorIds = requiredSensors(tasks);
|
|
37
|
+
const loaded = loadSensorsDetailed(root);
|
|
38
|
+
if (loaded.error || loaded.missing) return null;
|
|
39
|
+
const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
|
|
40
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
41
|
+
const identity = resolveEvidenceIdentity({
|
|
42
|
+
vaultBase, projectRoot: root, changeSlug: slug, sessionId, spawn,
|
|
43
|
+
});
|
|
44
|
+
const snapshot = captureGitSnapshot(root, { spawn });
|
|
45
|
+
const binding = evaluateEvidenceBinding(evidence, {
|
|
46
|
+
change_slug: slug,
|
|
47
|
+
identity,
|
|
48
|
+
snapshot,
|
|
49
|
+
tasks_sha256: tasksHashOf(tarefas),
|
|
50
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
51
|
+
sensor_config_sha256: sensorConfigSha256(loaded.sensors, sensorIds),
|
|
52
|
+
});
|
|
53
|
+
if (binding.state !== 'bound') return null;
|
|
54
|
+
const envelopeArtifact = (evidence.external_artifacts || []).find((item) => (
|
|
55
|
+
item.source === 'superpowers'
|
|
56
|
+
&& item.external_id === artifact.external_id
|
|
57
|
+
&& item.kind === artifact.kind
|
|
58
|
+
&& item.sha256 === artifact.sha256
|
|
59
|
+
&& item.authority === 'verified'
|
|
60
|
+
));
|
|
61
|
+
if (!envelopeArtifact) return null;
|
|
62
|
+
const currentArtifacts = collectBridgeArtifactEvidence({
|
|
63
|
+
projectRoot: root, tasks, sensors: evidence.sensors || [], spawn,
|
|
64
|
+
});
|
|
65
|
+
const currentArtifact = currentArtifacts.find((item) => item.external_id === artifact.external_id);
|
|
66
|
+
if (!currentArtifact || currentArtifact.authority !== 'verified'
|
|
67
|
+
|| canonicalBridgeJson(currentArtifact) !== canonicalBridgeJson(envelopeArtifact)) return null;
|
|
68
|
+
const sensor = (evidence.sensors || []).find((item) => item.id === envelopeArtifact.sensor_id);
|
|
69
|
+
const artifactResult = (sensor?.artifact_results || []).find((item) => (
|
|
70
|
+
item.external_id === artifact.external_id && item.path === envelopeArtifact.path
|
|
71
|
+
&& item.algorithm === 'sha256' && item.digest === artifact.sha256
|
|
72
|
+
));
|
|
73
|
+
if (!sensor || sensor.status !== 'green' || sensor.exit_code !== 0 || !artifactResult) return null;
|
|
74
|
+
|
|
75
|
+
const canonical = {
|
|
76
|
+
schema_version: 1,
|
|
77
|
+
contract_kind: 'proof',
|
|
78
|
+
type: 'evidence-envelope',
|
|
79
|
+
authority: 'verified',
|
|
80
|
+
external_id: artifact.external_id,
|
|
81
|
+
artifact_sha256: artifact.sha256,
|
|
82
|
+
origin: { tool: 'wendkeep', evidence_envelope_id: evidence.envelope_id },
|
|
83
|
+
provenance: { state: 'verified', source: 'wendkeep-evidence-envelope' },
|
|
84
|
+
evidence_envelope_id: evidence.envelope_id,
|
|
85
|
+
sensor_id: envelopeArtifact.sensor_id,
|
|
86
|
+
task_id: envelopeArtifact.task_id,
|
|
87
|
+
path: envelopeArtifact.path,
|
|
88
|
+
head_sha: evidence.head_sha,
|
|
89
|
+
git_blob: envelopeArtifact.git_blob,
|
|
90
|
+
manifest_git_blob: envelopeArtifact.manifest_git_blob,
|
|
91
|
+
};
|
|
92
|
+
canonical.proof_id = bridgeSha256(canonicalBridgeJson(canonical));
|
|
93
|
+
return issueCanonicalArtifactProof(canonical);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import * as bridgeFs from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
importSpecKitProjection,
|
|
7
|
+
buildSpecKitStatusProjection,
|
|
8
|
+
ingestSuperpowersArtifacts,
|
|
9
|
+
inspectEcosystemBridges,
|
|
10
|
+
inspectBridgeAdapterRoot,
|
|
11
|
+
readBridgeConfig,
|
|
12
|
+
verifyExternalArtifact,
|
|
13
|
+
isProjectContainedPath,
|
|
14
|
+
sealBridgeProjection,
|
|
15
|
+
bridgeDiagnostic,
|
|
16
|
+
} from '../packages/integrations/src/index.mjs';
|
|
17
|
+
import { buildCanonicalExternalTaskDispatch } from './task.mjs';
|
|
18
|
+
import { resolveProjectVault } from './project-vault.mjs';
|
|
19
|
+
import { readSpecKitBaseline, writeSpecKitBaseline } from './ecosystem-bridge-baseline.mjs';
|
|
20
|
+
import { deriveCanonicalArtifactProof } from './ecosystem-bridge-proof.mjs';
|
|
21
|
+
|
|
22
|
+
export const BRIDGE_HELP = `wendkeep bridge <status|import-spec-kit|export-status|dispatch-superpowers|verify-artifacts>
|
|
23
|
+
|
|
24
|
+
--project <path> consumer project (default: current directory)
|
|
25
|
+
--config <path> config path (default: .wendkeep/ecosystem-bridges.json)
|
|
26
|
+
--task-id <id> canonical WendKeep task selected from the active context
|
|
27
|
+
--task-contract <path> optional submitted copy; rejected when it differs from canonical state
|
|
28
|
+
--spec-projection <path> optional read-only Spec Kit projection
|
|
29
|
+
--change <slug> causal change holding the canonical Spec Kit baseline/evidence
|
|
30
|
+
--accept-baseline anchor the first green Spec Kit projection in the bound Vault
|
|
31
|
+
--input <path> external artifacts JSON for verify-artifacts
|
|
32
|
+
--proofs <path> artifact IDs to resolve from the canonical Evidence Envelope
|
|
33
|
+
--json emit typed JSON
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
function opt(argv, name) {
|
|
37
|
+
const index = argv.indexOf(name);
|
|
38
|
+
if (index >= 0) return argv[index + 1] || '';
|
|
39
|
+
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inside(root, target) {
|
|
43
|
+
return isProjectContainedPath(root, target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readProjectJson(projectRoot, pathValue, label, optional = false) {
|
|
47
|
+
if (!pathValue && optional) return null;
|
|
48
|
+
if (!pathValue) throw Object.assign(new Error(`${label} path is required`), { code: 'BRIDGE_ARGUMENT_INVALID' });
|
|
49
|
+
const rootReal = realpathSync(projectRoot);
|
|
50
|
+
const path = resolve(projectRoot, pathValue);
|
|
51
|
+
if (!inside(rootReal, path)) throw Object.assign(new Error(`${label} escapes the project`), { code: 'BRIDGE_INPUT_ESCAPE' });
|
|
52
|
+
if (!existsSync(path) || !lstatSync(path).isFile()) {
|
|
53
|
+
throw Object.assign(new Error(`${label} file not found`), { code: 'BRIDGE_INPUT_MISSING' });
|
|
54
|
+
}
|
|
55
|
+
const pathReal = realpathSync(path);
|
|
56
|
+
if (!inside(rootReal, pathReal)) throw Object.assign(new Error(`${label} escapes the project`), { code: 'BRIDGE_INPUT_ESCAPE' });
|
|
57
|
+
if (lstatSync(pathReal).size > 1024 * 1024) throw Object.assign(new Error(`${label} exceeds 1 MiB`), { code: 'BRIDGE_INPUT_LIMIT' });
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(pathReal, 'utf8'));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw Object.assign(new Error(`${label} is not valid JSON: ${error.message}`), { code: 'BRIDGE_INPUT_INVALID' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function write(value, json) {
|
|
66
|
+
process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${JSON.stringify(value, null, 2)}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function runEcosystemBridge(argv = []) {
|
|
70
|
+
const sub = argv[0];
|
|
71
|
+
if (!sub || ['help', '--help', '-h'].includes(sub)) {
|
|
72
|
+
process.stdout.write(BRIDGE_HELP);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const json = argv.includes('--json');
|
|
76
|
+
const projectRoot = resolve(opt(argv, '--project') || process.cwd());
|
|
77
|
+
try {
|
|
78
|
+
if (sub === 'status') {
|
|
79
|
+
const result = inspectEcosystemBridges({ projectRoot, configPath: opt(argv, '--config'), fs: bridgeFs });
|
|
80
|
+
write(result, json);
|
|
81
|
+
return result.ok ? 0 : 1;
|
|
82
|
+
}
|
|
83
|
+
const loaded = readBridgeConfig(projectRoot, opt(argv, '--config'), { fs: bridgeFs });
|
|
84
|
+
if (sub === 'import-spec-kit') {
|
|
85
|
+
if (opt(argv, '--previous')) {
|
|
86
|
+
throw Object.assign(new Error('--previous cannot replace the canonical Vault baseline'), { code: 'BRIDGE_ARGUMENT_INVALID' });
|
|
87
|
+
}
|
|
88
|
+
const adapter = loaded.config.adapters['spec-kit'];
|
|
89
|
+
if (!adapter.enabled) {
|
|
90
|
+
const result = importSpecKitProjection({ projectRoot, config: loaded.config, fs: bridgeFs });
|
|
91
|
+
write(result, json);
|
|
92
|
+
return result.ok ? 0 : 1;
|
|
93
|
+
}
|
|
94
|
+
const changeSlug = opt(argv, '--change');
|
|
95
|
+
if (!changeSlug) throw Object.assign(new Error('--change is required for the canonical Spec Kit baseline'), { code: 'BRIDGE_BASELINE_CONTEXT_INVALID' });
|
|
96
|
+
const vault = resolveProjectVault({ startDir: projectRoot, explicitVault: opt(argv, '--vault') || '' }).base;
|
|
97
|
+
const baseline = readSpecKitBaseline(vault, changeSlug);
|
|
98
|
+
const current = importSpecKitProjection({ projectRoot, config: loaded.config, previousProjection: baseline, fs: bridgeFs });
|
|
99
|
+
let result = current;
|
|
100
|
+
if (argv.includes('--accept-baseline')) {
|
|
101
|
+
if (baseline && baseline.projection_id !== current.projection_id) {
|
|
102
|
+
result = sealBridgeProjection({
|
|
103
|
+
...current, ok: false,
|
|
104
|
+
diagnostics: [...current.diagnostics, bridgeDiagnostic('BRIDGE_BASELINE_STALE', {
|
|
105
|
+
adapter: 'spec-kit', message: 'canonical Spec Kit baseline already exists and cannot be replaced implicitly',
|
|
106
|
+
})],
|
|
107
|
+
});
|
|
108
|
+
} else if (!baseline && current.ok) writeSpecKitBaseline(vault, changeSlug, current);
|
|
109
|
+
} else if (!baseline) {
|
|
110
|
+
result = sealBridgeProjection({
|
|
111
|
+
...current, ok: false,
|
|
112
|
+
diagnostics: [...current.diagnostics, bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
|
|
113
|
+
adapter: 'spec-kit', message: 'anchor the first projection with --accept-baseline',
|
|
114
|
+
})],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
write(result, json);
|
|
118
|
+
return result.ok ? 0 : 1;
|
|
119
|
+
}
|
|
120
|
+
if (sub === 'export-status') {
|
|
121
|
+
const sourceProjection = readProjectJson(projectRoot, opt(argv, '--spec-projection'), 'Spec Kit projection');
|
|
122
|
+
const taskContract = readProjectJson(projectRoot, opt(argv, '--task-contract'), 'task contract', true);
|
|
123
|
+
const artifactInput = readProjectJson(projectRoot, opt(argv, '--input'), 'artifact input', true);
|
|
124
|
+
const result = buildSpecKitStatusProjection({
|
|
125
|
+
sourceProjection,
|
|
126
|
+
taskContracts: taskContract ? [taskContract] : [],
|
|
127
|
+
artifacts: artifactInput ? (Array.isArray(artifactInput) ? artifactInput : artifactInput.artifacts || []) : [],
|
|
128
|
+
});
|
|
129
|
+
write(result, json);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (sub === 'dispatch-superpowers') {
|
|
133
|
+
const taskId = opt(argv, '--task-id');
|
|
134
|
+
if (!taskId) throw Object.assign(new Error('task id is required'), { code: 'BRIDGE_ARGUMENT_INVALID' });
|
|
135
|
+
const submitted = readProjectJson(projectRoot, opt(argv, '--task-contract'), 'task contract', true);
|
|
136
|
+
const specKitProjection = readProjectJson(projectRoot, opt(argv, '--spec-projection'), 'Spec Kit projection', true);
|
|
137
|
+
const adapter = loaded.config.adapters.superpowers;
|
|
138
|
+
const adapterRoot = inspectBridgeAdapterRoot(projectRoot, adapter.root || '.superpowers', { adapter: 'superpowers', fs: bridgeFs });
|
|
139
|
+
if (!adapterRoot.valid) {
|
|
140
|
+
const result = {
|
|
141
|
+
schema_version: 1, adapter: 'superpowers', active: false, ok: false,
|
|
142
|
+
diagnostics: adapterRoot.diagnostics,
|
|
143
|
+
};
|
|
144
|
+
write(result, json);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const present = adapterRoot.present;
|
|
148
|
+
const vault = resolveProjectVault({
|
|
149
|
+
startDir: projectRoot,
|
|
150
|
+
explicitVault: opt(argv, '--vault') || '',
|
|
151
|
+
}).base;
|
|
152
|
+
const changeSlug = opt(argv, '--change');
|
|
153
|
+
let baselineProjection = null;
|
|
154
|
+
if (loaded.config.adapters['spec-kit'].enabled) {
|
|
155
|
+
if (!changeSlug || !specKitProjection) {
|
|
156
|
+
const result = {
|
|
157
|
+
schema_version: 1, adapter: 'superpowers', active: true, ok: false,
|
|
158
|
+
diagnostics: [bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
|
|
159
|
+
adapter: 'spec-kit', message: 'dispatch requires --change and --spec-projection when Spec Kit is active',
|
|
160
|
+
})],
|
|
161
|
+
};
|
|
162
|
+
write(result, json);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
baselineProjection = readSpecKitBaseline(vault, changeSlug);
|
|
166
|
+
if (!baselineProjection) {
|
|
167
|
+
const result = {
|
|
168
|
+
schema_version: 1, adapter: 'superpowers', active: true, ok: false,
|
|
169
|
+
diagnostics: [bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
|
|
170
|
+
adapter: 'spec-kit', message: 'canonical Spec Kit baseline is missing from the bound Vault change',
|
|
171
|
+
})],
|
|
172
|
+
};
|
|
173
|
+
write(result, json);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const authorityArgs = ['--project', projectRoot, '--vault', vault, '--task-id', taskId];
|
|
178
|
+
for (const name of ['--session', '--change']) {
|
|
179
|
+
const value = opt(argv, name);
|
|
180
|
+
if (value) authorityArgs.push(name, value);
|
|
181
|
+
}
|
|
182
|
+
const result = buildCanonicalExternalTaskDispatch({
|
|
183
|
+
adapter: 'superpowers',
|
|
184
|
+
authorityArgv: authorityArgs,
|
|
185
|
+
taskId,
|
|
186
|
+
submittedTaskContract: submitted,
|
|
187
|
+
projectRoot,
|
|
188
|
+
specKitProjection,
|
|
189
|
+
baselineProjection,
|
|
190
|
+
config: loaded.config,
|
|
191
|
+
detectedVersion: adapter.version || '',
|
|
192
|
+
present,
|
|
193
|
+
});
|
|
194
|
+
write(result, json);
|
|
195
|
+
return result.ok ? 0 : 1;
|
|
196
|
+
}
|
|
197
|
+
if (sub === 'verify-artifacts') {
|
|
198
|
+
const input = readProjectJson(projectRoot, opt(argv, '--input'), 'artifact input');
|
|
199
|
+
const proofs = readProjectJson(projectRoot, opt(argv, '--proofs'), 'proof input');
|
|
200
|
+
const artifacts = ingestSuperpowersArtifacts(Array.isArray(input) ? input : input.artifacts);
|
|
201
|
+
const proofList = (Array.isArray(proofs) ? proofs : proofs.proofs) || [];
|
|
202
|
+
const canonicalRequested = proofList.some((proof) => proof?.type === 'evidence-envelope');
|
|
203
|
+
const changeSlug = opt(argv, '--change');
|
|
204
|
+
const vault = canonicalRequested && changeSlug
|
|
205
|
+
? resolveProjectVault({ startDir: projectRoot, explicitVault: opt(argv, '--vault') || '' }).base
|
|
206
|
+
: '';
|
|
207
|
+
const verified = artifacts.map((artifact) => {
|
|
208
|
+
const artifactProofs = proofList.filter((proof) => (
|
|
209
|
+
!proof.external_id || proof.external_id === artifact.external_id
|
|
210
|
+
));
|
|
211
|
+
const canonicalProofs = vault ? artifactProofs.map((proof) => deriveCanonicalArtifactProof({
|
|
212
|
+
artifact, proof, projectRoot, vaultBase: vault, changeSlug,
|
|
213
|
+
sessionId: opt(argv, '--session') || '',
|
|
214
|
+
})).filter(Boolean) : [];
|
|
215
|
+
return verifyExternalArtifact(artifact, { proofs: artifactProofs, canonicalProofs });
|
|
216
|
+
});
|
|
217
|
+
const result = { schema_version: 1, ok: verified.every((item) => item.authority === 'verified'), artifacts: verified };
|
|
218
|
+
write(result, json);
|
|
219
|
+
return result.ok ? 0 : 1;
|
|
220
|
+
}
|
|
221
|
+
throw Object.assign(new Error(`unknown bridge subcommand: ${sub}`), { code: 'BRIDGE_SUBCOMMAND_UNKNOWN' });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const payload = { ok: false, code: error?.code || 'BRIDGE_COMMAND_FAILED', error: error?.message || String(error) };
|
|
224
|
+
process.stderr.write(json ? `${JSON.stringify(payload)}\n` : `wendkeep bridge: ${payload.code}: ${payload.error}\n`);
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -268,6 +268,7 @@ export function buildEvidenceEnvelope({
|
|
|
268
268
|
version = '',
|
|
269
269
|
runtimePlatform = `${process.platform}-${process.arch}`,
|
|
270
270
|
hostCoverage = null,
|
|
271
|
+
externalArtifacts = [],
|
|
271
272
|
} = {}) {
|
|
272
273
|
const envelope = {
|
|
273
274
|
schema_version: 2,
|
|
@@ -287,6 +288,7 @@ export function buildEvidenceEnvelope({
|
|
|
287
288
|
started_at: startedAt,
|
|
288
289
|
finished_at: finishedAt,
|
|
289
290
|
sensors,
|
|
291
|
+
...(externalArtifacts.length ? { external_artifacts: structuredClone(externalArtifacts) } : {}),
|
|
290
292
|
...(hostCoverage ? { host_coverage: structuredClone(hostCoverage) } : {}),
|
|
291
293
|
tdd_attestations: tddAttestations,
|
|
292
294
|
};
|
package/src/task-contracts.mjs
CHANGED
|
@@ -43,6 +43,25 @@ function uniqueStrings(values) {
|
|
|
43
43
|
return [...new Set(list.map((value) => String(value).trim()).filter(Boolean))];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function normalizeExternalArtifactEvidence(input = {}) {
|
|
47
|
+
const source = String(input.source || '').trim();
|
|
48
|
+
const externalId = String(input.external_id || '').trim();
|
|
49
|
+
const kind = String(input.kind || '').trim();
|
|
50
|
+
const sha256Value = String(input.sha256 || '').trim();
|
|
51
|
+
if (!source || !externalId || !['artifact', 'review', 'commit'].includes(kind)
|
|
52
|
+
|| !/^[a-f0-9]{64}$/.test(sha256Value)) {
|
|
53
|
+
throw Object.assign(new Error('external artifact evidence is incomplete'), { code: 'TASK_EXTERNAL_ARTIFACT_INVALID' });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
schema_version: 1,
|
|
57
|
+
source,
|
|
58
|
+
external_id: externalId,
|
|
59
|
+
kind,
|
|
60
|
+
sha256: sha256Value,
|
|
61
|
+
authority: 'reported',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
function bindingFrom(input) {
|
|
47
66
|
return {
|
|
48
67
|
project_id: String(input.projectId || ''),
|