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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { ECOSYSTEM_ADAPTER_MANIFESTS, isVersionInRange } from './capabilities.mjs';
|
|
4
|
+
import { bridgeDiagnostic, bridgeError } from './bridge-diagnostics.mjs';
|
|
5
|
+
|
|
6
|
+
const ADAPTERS = ['spec-kit', 'superpowers'];
|
|
7
|
+
|
|
8
|
+
export function isProjectContainedPath(projectRoot, targetPath, pathApi = { relative, isAbsolute, sep }) {
|
|
9
|
+
const rel = pathApi.relative(projectRoot, targetPath);
|
|
10
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(rel));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function inspectBridgeAdapterRoot(projectRoot, configuredRoot, { adapter = '', fs = null } = {}) {
|
|
14
|
+
const invalid = (path, message) => ({
|
|
15
|
+
valid: false,
|
|
16
|
+
present: false,
|
|
17
|
+
path,
|
|
18
|
+
diagnostics: [bridgeDiagnostic('BRIDGE_SOURCE_INVALID', {
|
|
19
|
+
adapter, path, message,
|
|
20
|
+
})],
|
|
21
|
+
});
|
|
22
|
+
if (!fs || ['existsSync', 'lstatSync', 'realpathSync'].some((name) => typeof fs[name] !== 'function')) {
|
|
23
|
+
return invalid(resolve(projectRoot), 'bridge filesystem capability is unavailable');
|
|
24
|
+
}
|
|
25
|
+
const { existsSync, lstatSync, realpathSync } = fs;
|
|
26
|
+
let root;
|
|
27
|
+
try {
|
|
28
|
+
root = realpathSync(resolve(projectRoot));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return invalid(resolve(projectRoot), `project root is not a real directory: ${error?.message || error}`);
|
|
31
|
+
}
|
|
32
|
+
const candidate = resolve(root, configuredRoot || `.${adapter}`);
|
|
33
|
+
if (!isProjectContainedPath(root, candidate)) {
|
|
34
|
+
return invalid(candidate, `${adapter} root must stay inside the project`);
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(candidate)) return { valid: true, present: false, path: candidate, diagnostics: [] };
|
|
37
|
+
try {
|
|
38
|
+
const real = realpathSync(candidate);
|
|
39
|
+
if (!isProjectContainedPath(root, real)) {
|
|
40
|
+
return invalid(candidate, `${adapter} root symlink escapes the project`);
|
|
41
|
+
}
|
|
42
|
+
if (!lstatSync(real).isDirectory()) {
|
|
43
|
+
return invalid(candidate, `${adapter} root must be a directory`);
|
|
44
|
+
}
|
|
45
|
+
return { valid: true, present: true, path: real, diagnostics: [] };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return invalid(candidate, `${adapter} root is unsafe: ${error?.message || error}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeBridgeConfig(input = {}) {
|
|
52
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
53
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', 'bridge config must be an object');
|
|
54
|
+
}
|
|
55
|
+
if (input.schema_version !== undefined && input.schema_version !== 1) {
|
|
56
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', 'bridge config requires schema_version 1');
|
|
57
|
+
}
|
|
58
|
+
const configured = input.adapters || {};
|
|
59
|
+
const unknown = Object.keys(configured).filter((name) => !ADAPTERS.includes(name));
|
|
60
|
+
if (unknown.length) throw bridgeError('BRIDGE_CONFIG_INVALID', `unknown bridge adapter: ${unknown.join(', ')}`);
|
|
61
|
+
const adapters = {};
|
|
62
|
+
for (const name of ADAPTERS) {
|
|
63
|
+
const value = configured[name] || {};
|
|
64
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
65
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', `adapter config must be an object: ${name}`);
|
|
66
|
+
}
|
|
67
|
+
const ownershipClaims = value.ownership_claims === undefined ? [] : value.ownership_claims;
|
|
68
|
+
if (!Array.isArray(ownershipClaims) || ownershipClaims.some((claim) => (
|
|
69
|
+
!claim || typeof claim !== 'object' || Array.isArray(claim)
|
|
70
|
+
|| !String(claim.concept || '').trim() || !String(claim.owner || '').trim()
|
|
71
|
+
))) {
|
|
72
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', `ownership_claims must contain concept/owner pairs: ${name}`);
|
|
73
|
+
}
|
|
74
|
+
adapters[name] = {
|
|
75
|
+
enabled: value.enabled === true,
|
|
76
|
+
...(value.root ? { root: String(value.root) } : {}),
|
|
77
|
+
...(value.version ? { version: String(value.version) } : {}),
|
|
78
|
+
...(ownershipClaims.length ? {
|
|
79
|
+
ownership_claims: ownershipClaims.map((claim) => ({
|
|
80
|
+
concept: String(claim.concept).trim(), owner: String(claim.owner).trim(),
|
|
81
|
+
})),
|
|
82
|
+
} : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { schema_version: 1, adapters };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readBridgeConfig(projectRoot, configPath = '', { fs = null } = {}) {
|
|
89
|
+
if (!fs || ['existsSync', 'readFileSync', 'realpathSync'].some((name) => typeof fs[name] !== 'function')) {
|
|
90
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', 'bridge filesystem capability is unavailable');
|
|
91
|
+
}
|
|
92
|
+
const { existsSync, readFileSync, realpathSync } = fs;
|
|
93
|
+
const root = realpathSync(resolve(projectRoot));
|
|
94
|
+
const path = resolve(root, configPath || '.wendkeep/ecosystem-bridges.json');
|
|
95
|
+
if (!isProjectContainedPath(root, path)) {
|
|
96
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', 'bridge config must stay inside the project', { path });
|
|
97
|
+
}
|
|
98
|
+
if (!existsSync(path)) return { path, exists: false, config: normalizeBridgeConfig({}) };
|
|
99
|
+
const real = realpathSync(path);
|
|
100
|
+
if (!isProjectContainedPath(root, real)) {
|
|
101
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', 'bridge config symlink escapes the project', { path });
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return { path: real, exists: true, config: normalizeBridgeConfig(JSON.parse(readFileSync(real, 'utf8'))) };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code === 'BRIDGE_CONFIG_INVALID') throw error;
|
|
107
|
+
throw bridgeError('BRIDGE_CONFIG_INVALID', `invalid bridge config: ${error?.message || error}`, { path });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function assessBridgeAdapter(name, { config = normalizeBridgeConfig({}), detectedVersion = '', present = true } = {}) {
|
|
112
|
+
const manifest = ECOSYSTEM_ADAPTER_MANIFESTS[name];
|
|
113
|
+
if (!manifest) {
|
|
114
|
+
return { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_ADAPTER_UNKNOWN', { adapter: name })] };
|
|
115
|
+
}
|
|
116
|
+
const adapter = config.adapters[name] || { enabled: false };
|
|
117
|
+
if (!adapter.enabled) {
|
|
118
|
+
return { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_ADAPTER_DISABLED', {
|
|
119
|
+
adapter: name, blocking: false, message: `${name} adapter is disabled`,
|
|
120
|
+
})] };
|
|
121
|
+
}
|
|
122
|
+
if (!present) {
|
|
123
|
+
return { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_ADAPTER_MISSING', {
|
|
124
|
+
adapter: name, message: `${name} adapter source was not found`,
|
|
125
|
+
})] };
|
|
126
|
+
}
|
|
127
|
+
const version = detectedVersion || adapter.version || '';
|
|
128
|
+
if (!version) {
|
|
129
|
+
return { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_VERSION_MISSING', {
|
|
130
|
+
adapter: name, expected: manifest.compatibility_range,
|
|
131
|
+
})] };
|
|
132
|
+
}
|
|
133
|
+
if (!isVersionInRange(version, manifest.compatibility_range)) {
|
|
134
|
+
return { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_VERSION_INCOMPATIBLE', {
|
|
135
|
+
adapter: name, expected: manifest.compatibility_range, observed: version,
|
|
136
|
+
})] };
|
|
137
|
+
}
|
|
138
|
+
return { available: true, adapter: name, version, manifest, diagnostics: [] };
|
|
139
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { ECOSYSTEM_ADAPTER_MANIFESTS, isVersionInRange } from './capabilities.mjs';
|
|
4
|
+
import { bridgeDiagnostic, bridgeError } from './bridge-diagnostics.mjs';
|
|
5
|
+
|
|
6
|
+
export const ECOSYSTEM_BRIDGE_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
const SPEC_PROJECTION_ADAPTER = 'spec-kit';
|
|
9
|
+
const REFERENCE_KINDS = new Set(['constitution', 'spec', 'plan', 'task', 'artifact', 'review', 'commit']);
|
|
10
|
+
const MAPPING_SOURCE_KINDS = new Set(['story', 'requirement']);
|
|
11
|
+
|
|
12
|
+
export const BRIDGE_AUTHORITY_MATRIX = Object.freeze({
|
|
13
|
+
spec_source: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['spec-kit'], external_authority: 'reported' }),
|
|
14
|
+
plan: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['spec-kit'], external_authority: 'reported' }),
|
|
15
|
+
task: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['spec-kit', 'superpowers'], external_authority: 'reported' }),
|
|
16
|
+
execution: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['superpowers'], external_authority: 'reported' }),
|
|
17
|
+
artifact: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['superpowers'], external_authority: 'reported' }),
|
|
18
|
+
evidence: Object.freeze({ canonical_owner: 'wendkeep', adapters: ['superpowers'], external_authority: 'reported' }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function canonicalBridgeJson(value) {
|
|
22
|
+
if (Array.isArray(value)) return `[${value.map(canonicalBridgeJson).join(',')}]`;
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalBridgeJson(value[key])}`).join(',')}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function bridgeSha256(value) {
|
|
30
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8');
|
|
31
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateBridgeOwnership({ adapter = '', claims = [] } = {}) {
|
|
35
|
+
const diagnostics = [];
|
|
36
|
+
for (const claim of Array.isArray(claims) ? claims : []) {
|
|
37
|
+
const concept = String(claim?.concept || '');
|
|
38
|
+
const owner = String(claim?.owner || '');
|
|
39
|
+
const policy = BRIDGE_AUTHORITY_MATRIX[concept];
|
|
40
|
+
if (!policy || owner !== policy.canonical_owner) {
|
|
41
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_OWNERSHIP_CONFLICT', {
|
|
42
|
+
adapter,
|
|
43
|
+
expected: policy?.canonical_owner || 'wendkeep',
|
|
44
|
+
observed: owner || '(missing)',
|
|
45
|
+
message: `external adapter cannot own canonical ${concept || 'state'}`,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function projectionIdentity(value) {
|
|
53
|
+
const { projection_id: ignoredId, ...identity } = value || {};
|
|
54
|
+
return identity;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validSha(value, size = 64) {
|
|
58
|
+
return new RegExp(`^[a-f0-9]{${size}}$`).test(String(value || ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validReportedMetadata(value) {
|
|
62
|
+
return Boolean(value?.origin?.tool && value?.provenance?.state === 'reported' && value?.provenance?.source);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validVerifiedMetadata(value) {
|
|
66
|
+
return Boolean(value?.origin?.tool === 'wendkeep'
|
|
67
|
+
&& value?.provenance?.state === 'verified'
|
|
68
|
+
&& value?.provenance?.source === 'wendkeep-evidence-envelope');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasExactKeys(value, expected) {
|
|
72
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value)
|
|
73
|
+
&& Object.keys(value).sort().join('\0') === [...expected].sort().join('\0'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createBridgeProjection({
|
|
77
|
+
adapter, adapterVersion, sourceRoot = '', references = [], mappings = [], claims = [],
|
|
78
|
+
} = {}) {
|
|
79
|
+
const ownership = validateBridgeOwnership({ adapter, claims });
|
|
80
|
+
const manifest = ECOSYSTEM_ADAPTER_MANIFESTS[String(adapter || '')];
|
|
81
|
+
const projection = {
|
|
82
|
+
schema_version: ECOSYSTEM_BRIDGE_SCHEMA_VERSION,
|
|
83
|
+
contract_kind: 'spec-projection',
|
|
84
|
+
adapter: String(adapter || ''),
|
|
85
|
+
adapter_version: String(adapterVersion || ''),
|
|
86
|
+
source_root: String(sourceRoot || '').replaceAll('\\', '/'),
|
|
87
|
+
authority: 'reported',
|
|
88
|
+
origin: {
|
|
89
|
+
tool: String(adapter || ''), version: String(adapterVersion || ''),
|
|
90
|
+
root: String(sourceRoot || '').replaceAll('\\', '/'),
|
|
91
|
+
},
|
|
92
|
+
compatibility: {
|
|
93
|
+
range: String(manifest?.compatibility_range || ''),
|
|
94
|
+
detected_version: String(adapterVersion || ''),
|
|
95
|
+
supported: Boolean(manifest && isVersionInRange(adapterVersion, manifest.compatibility_range)),
|
|
96
|
+
},
|
|
97
|
+
provenance: { state: 'reported', source: 'external-read-only' },
|
|
98
|
+
ownership: claims.map((claim) => ({ concept: String(claim.concept || ''), owner: String(claim.owner || '') })),
|
|
99
|
+
references: references.map((item) => ({
|
|
100
|
+
kind: String(item.kind || ''),
|
|
101
|
+
source_id: String(item.source_id || ''),
|
|
102
|
+
path: String(item.path || '').replaceAll('\\', '/'),
|
|
103
|
+
sha256: String(item.sha256 || ''),
|
|
104
|
+
authority: 'reported',
|
|
105
|
+
...(item.title ? { title: String(item.title).slice(0, 300) } : {}),
|
|
106
|
+
})),
|
|
107
|
+
mappings: mappings.map((item) => ({
|
|
108
|
+
source_id: String(item.source_id || ''),
|
|
109
|
+
source_kind: String(item.source_kind || ''),
|
|
110
|
+
capability: String(item.capability || ''),
|
|
111
|
+
change_slug: String(item.change_slug || ''),
|
|
112
|
+
task_ids: [...new Set(Array.isArray(item.task_ids) ? item.task_ids.map(String) : [])],
|
|
113
|
+
})),
|
|
114
|
+
diagnostics: ownership.diagnostics,
|
|
115
|
+
};
|
|
116
|
+
const sealed = sealBridgeProjection({ ...projection, ok: ownership.ok });
|
|
117
|
+
const validation = validateBridgeProjection(sealed);
|
|
118
|
+
const contractDiagnostics = validation.diagnostics.filter((item) => item.code === 'BRIDGE_PROJECTION_INVALID');
|
|
119
|
+
if (contractDiagnostics.length) {
|
|
120
|
+
throw bridgeError('BRIDGE_PROJECTION_INVALID', contractDiagnostics[0].message, {
|
|
121
|
+
adapter: String(adapter || ''), diagnostics: contractDiagnostics,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return sealed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function sealBridgeProjection(value) {
|
|
128
|
+
const projection = structuredClone(value || {});
|
|
129
|
+
delete projection.projection_id;
|
|
130
|
+
return {
|
|
131
|
+
...projection,
|
|
132
|
+
projection_id: bridgeSha256(canonicalBridgeJson(projectionIdentity(projection))),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function validateBridgeProjection(value) {
|
|
137
|
+
const diagnostics = [];
|
|
138
|
+
const fail = (message) => diagnostics.push(bridgeDiagnostic('BRIDGE_PROJECTION_INVALID', {
|
|
139
|
+
adapter: String(value?.adapter || ''), message,
|
|
140
|
+
}));
|
|
141
|
+
if (value?.schema_version !== 1 || value?.contract_kind !== 'spec-projection') fail('projection contract kind/schema is invalid');
|
|
142
|
+
const manifest = ECOSYSTEM_ADAPTER_MANIFESTS[SPEC_PROJECTION_ADAPTER];
|
|
143
|
+
if (value?.adapter !== SPEC_PROJECTION_ADAPTER) fail('spec projection adapter must be spec-kit');
|
|
144
|
+
if (typeof value?.adapter_version !== 'string' || !value.adapter_version
|
|
145
|
+
|| !value?.origin || !value?.compatibility || !value?.provenance) fail('projection metadata is incomplete');
|
|
146
|
+
if (value?.authority !== 'reported' || value?.provenance?.state !== 'reported'
|
|
147
|
+
|| value?.provenance?.source !== 'external-read-only') fail('external projection cannot be authoritative');
|
|
148
|
+
if (!hasExactKeys(value?.provenance, ['state', 'source'])) fail('projection provenance shape is invalid');
|
|
149
|
+
if (value?.origin?.tool !== SPEC_PROJECTION_ADAPTER || value?.origin?.version !== value?.adapter_version
|
|
150
|
+
|| typeof value?.origin?.root !== 'string') fail('projection origin does not match its adapter/version');
|
|
151
|
+
if (!hasExactKeys(value?.compatibility, ['range', 'detected_version', 'supported'])
|
|
152
|
+
|| value?.compatibility?.range !== manifest.compatibility_range
|
|
153
|
+
|| value?.compatibility?.detected_version !== value?.adapter_version
|
|
154
|
+
|| value?.compatibility?.supported !== true
|
|
155
|
+
|| !isVersionInRange(value?.adapter_version, manifest.compatibility_range)) {
|
|
156
|
+
fail('projection adapter version is incompatible');
|
|
157
|
+
}
|
|
158
|
+
const decisionFieldsValid = Array.isArray(value?.diagnostics) && typeof value?.ok === 'boolean';
|
|
159
|
+
if (!decisionFieldsValid) fail('projection decision fields are invalid');
|
|
160
|
+
if (decisionFieldsValid) {
|
|
161
|
+
const malformedDiagnostic = value.diagnostics.some((item) => (
|
|
162
|
+
!item || typeof item !== 'object' || Array.isArray(item)
|
|
163
|
+
|| typeof item.code !== 'string' || !item.code
|
|
164
|
+
|| typeof item.blocking !== 'boolean'
|
|
165
|
+
|| typeof item.message !== 'string' || !item.message
|
|
166
|
+
));
|
|
167
|
+
if (malformedDiagnostic) fail('projection diagnostics are malformed');
|
|
168
|
+
const hasBlockingDiagnostic = value.diagnostics.some((item) => item?.blocking === true);
|
|
169
|
+
if (value.ok === hasBlockingDiagnostic) fail('projection ok decision does not match blocking diagnostics');
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(value?.ownership)) fail('projection ownership is invalid');
|
|
172
|
+
if (!Array.isArray(value?.references) || value.references.some((item) => (
|
|
173
|
+
!REFERENCE_KINDS.has(item?.kind) || typeof item?.source_id !== 'string' || !item.source_id
|
|
174
|
+
|| typeof item?.path !== 'string' || !item.path || !validSha(item?.sha256) || item?.authority !== 'reported'
|
|
175
|
+
|| (item?.title !== undefined && (typeof item.title !== 'string' || item.title.length > 300))
|
|
176
|
+
))) fail('projection references are invalid');
|
|
177
|
+
if (!Array.isArray(value?.mappings) || value.mappings.some((item) => (
|
|
178
|
+
!hasExactKeys(item, ['source_id', 'source_kind', 'capability', 'change_slug', 'task_ids'])
|
|
179
|
+
|| typeof item?.source_id !== 'string' || !item.source_id || !MAPPING_SOURCE_KINDS.has(item?.source_kind)
|
|
180
|
+
|| typeof item?.capability !== 'string' || !item.capability
|
|
181
|
+
|| typeof item?.change_slug !== 'string' || !item.change_slug
|
|
182
|
+
|| !Array.isArray(item?.task_ids) || item.task_ids.some((taskId) => typeof taskId !== 'string' || !taskId)
|
|
183
|
+
|| new Set(item.task_ids).size !== item.task_ids.length
|
|
184
|
+
))) fail('projection mappings are invalid');
|
|
185
|
+
const ownership = validateBridgeOwnership({ adapter: value?.adapter, claims: value?.ownership });
|
|
186
|
+
diagnostics.push(...ownership.diagnostics);
|
|
187
|
+
const expected = bridgeSha256(canonicalBridgeJson(projectionIdentity(value)));
|
|
188
|
+
if (!validSha(value?.projection_id) || value.projection_id !== expected) fail('projection_id does not match canonical projection bytes');
|
|
189
|
+
return { valid: diagnostics.length === 0, diagnostics, expected_projection_id: expected };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function validateBridgeRuntimeEnvelope(value) {
|
|
193
|
+
const diagnostics = [];
|
|
194
|
+
const invalid = (message) => diagnostics.push(bridgeDiagnostic('BRIDGE_SCHEMA_INVALID', {
|
|
195
|
+
adapter: String(value?.adapter || ''), message,
|
|
196
|
+
}));
|
|
197
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.schema_version !== 1) {
|
|
198
|
+
invalid('bridge envelope must be a schema v1 object');
|
|
199
|
+
return { valid: false, diagnostics };
|
|
200
|
+
}
|
|
201
|
+
const kind = String(value.contract_kind || '');
|
|
202
|
+
if (kind === 'spec-projection') {
|
|
203
|
+
const projection = validateBridgeProjection(value);
|
|
204
|
+
return {
|
|
205
|
+
valid: projection.valid,
|
|
206
|
+
diagnostics: projection.valid ? [] : projection.diagnostics.map((item) => bridgeDiagnostic('BRIDGE_SCHEMA_INVALID', {
|
|
207
|
+
adapter: value.adapter, message: item.message,
|
|
208
|
+
})),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (kind === 'dispatch') {
|
|
212
|
+
if (value.adapter !== 'superpowers' || value.authority !== 'reported' || value.canonical_owner !== 'wendkeep'
|
|
213
|
+
|| !validReportedMetadata(value) || !value.compatibility?.range || value.compatibility?.supported !== true
|
|
214
|
+
|| !value.task_contract?.contract_id
|
|
215
|
+
|| !value.task_contract?.binding?.active_context_id || !value.worktree?.provider || !validSha(value.dispatch_id)) {
|
|
216
|
+
invalid('dispatch envelope is incomplete');
|
|
217
|
+
}
|
|
218
|
+
const { dispatch_id: ignoredId, ok: ignoredOk, ...identity } = value;
|
|
219
|
+
if (validSha(value.dispatch_id) && bridgeSha256(canonicalBridgeJson(identity)) !== value.dispatch_id) {
|
|
220
|
+
invalid('dispatch_id does not match canonical dispatch bytes');
|
|
221
|
+
}
|
|
222
|
+
} else if (kind === 'handoff') {
|
|
223
|
+
if (!value.handoff_id || !value.task_contract_id || !validSha(value.head_sha, 40)
|
|
224
|
+
|| value.authority !== 'reported' || !validReportedMetadata(value)) invalid('handoff envelope is incomplete');
|
|
225
|
+
} else if (kind === 'external-artifact') {
|
|
226
|
+
const authorityValid = value.authority === 'reported'
|
|
227
|
+
? validReportedMetadata(value)
|
|
228
|
+
: (value.authority === 'verified' && validVerifiedMetadata(value)
|
|
229
|
+
&& validateBridgeRuntimeEnvelope(value.proof).valid
|
|
230
|
+
&& value.proof.artifact_sha256 === value.sha256);
|
|
231
|
+
if (value.source !== 'superpowers' || !value.external_id || !['artifact', 'review', 'commit'].includes(value.kind)
|
|
232
|
+
|| !validSha(value.sha256) || !authorityValid) invalid('external artifact envelope is invalid');
|
|
233
|
+
} else if (kind === 'proof') {
|
|
234
|
+
const { proof_id: ignoredId, ...identity } = value;
|
|
235
|
+
if (value.type !== 'evidence-envelope' || !validSha(value.artifact_sha256)
|
|
236
|
+
|| value.authority !== 'verified' || !validVerifiedMetadata(value)
|
|
237
|
+
|| !validSha(value.proof_id) || bridgeSha256(canonicalBridgeJson(identity)) !== value.proof_id
|
|
238
|
+
|| !String(value.evidence_envelope_id || '').match(/^sha256:[a-f0-9]{64}$/)
|
|
239
|
+
|| value.origin?.evidence_envelope_id !== value.evidence_envelope_id
|
|
240
|
+
|| !value.external_id || !value.sensor_id || !value.task_id || !value.path
|
|
241
|
+
|| !validSha(value.head_sha, 40) || !validSha(value.git_blob, 40) || !validSha(value.manifest_git_blob, 40)) {
|
|
242
|
+
invalid('proof envelope is invalid');
|
|
243
|
+
}
|
|
244
|
+
} else if (kind === 'status-projection') {
|
|
245
|
+
if (value.adapter !== 'spec-kit' || value.authority !== 'reported' || value.canonical_owner !== 'wendkeep'
|
|
246
|
+
|| !validReportedMetadata(value) || !validSha(value.source_projection_id)
|
|
247
|
+
|| !Array.isArray(value.tasks) || !Array.isArray(value.evidence)) invalid('status projection is invalid');
|
|
248
|
+
const { status_projection_id: ignoredId, ...identity } = value;
|
|
249
|
+
if (!validSha(value.status_projection_id)
|
|
250
|
+
|| bridgeSha256(canonicalBridgeJson(identity)) !== value.status_projection_id) invalid('status_projection_id is invalid');
|
|
251
|
+
} else {
|
|
252
|
+
invalid(`unknown bridge contract_kind: ${kind || '(missing)'}`);
|
|
253
|
+
}
|
|
254
|
+
return { valid: diagnostics.length === 0, diagnostics };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function detectBridgeDrift(previous, current) {
|
|
258
|
+
const before = new Map((previous?.references || []).map((item) => [item.source_id, item]));
|
|
259
|
+
const after = new Map((current?.references || []).map((item) => [item.source_id, item]));
|
|
260
|
+
const diagnostics = [];
|
|
261
|
+
for (const item of current?.references || []) {
|
|
262
|
+
const expected = before.get(item.source_id);
|
|
263
|
+
if (!before.has(item.source_id)) {
|
|
264
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_DRIFT', {
|
|
265
|
+
adapter: current.adapter,
|
|
266
|
+
path: item.path,
|
|
267
|
+
expected: '(absent)',
|
|
268
|
+
observed: 'new',
|
|
269
|
+
message: `new external source appeared: ${item.source_id}`,
|
|
270
|
+
}));
|
|
271
|
+
} else if (canonicalBridgeJson({ kind: expected.kind, path: expected.path, sha256: expected.sha256 })
|
|
272
|
+
!== canonicalBridgeJson({ kind: item.kind, path: item.path, sha256: item.sha256 })) {
|
|
273
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_DRIFT', {
|
|
274
|
+
adapter: current.adapter,
|
|
275
|
+
path: item.path,
|
|
276
|
+
expected: canonicalBridgeJson({ kind: expected.kind, path: expected.path, sha256: expected.sha256 }),
|
|
277
|
+
observed: canonicalBridgeJson({ kind: item.kind, path: item.path, sha256: item.sha256 }),
|
|
278
|
+
message: `external source path/kind/hash drifted: ${item.source_id}`,
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const item of previous?.references || []) {
|
|
283
|
+
if (!after.has(item.source_id)) {
|
|
284
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_DRIFT', {
|
|
285
|
+
adapter: current.adapter,
|
|
286
|
+
path: item.path,
|
|
287
|
+
expected: canonicalBridgeJson({ kind: item.kind, path: item.path, sha256: item.sha256 }),
|
|
288
|
+
observed: '(missing)',
|
|
289
|
+
message: `external source disappeared: ${item.source_id}`,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const beforeMappings = new Map((previous?.mappings || []).map((item) => [item.source_id, item]));
|
|
294
|
+
const afterMappings = new Map((current?.mappings || []).map((item) => [item.source_id, item]));
|
|
295
|
+
for (const [sourceId, mapping] of afterMappings) {
|
|
296
|
+
const expected = beforeMappings.get(sourceId);
|
|
297
|
+
if (!expected || canonicalBridgeJson(expected) !== canonicalBridgeJson(mapping)) {
|
|
298
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_DRIFT', {
|
|
299
|
+
adapter: current?.adapter,
|
|
300
|
+
expected: expected ? canonicalBridgeJson(expected) : '(absent)',
|
|
301
|
+
observed: canonicalBridgeJson(mapping),
|
|
302
|
+
message: `external capability/change/task mapping drifted: ${sourceId}`,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const [sourceId, mapping] of beforeMappings) {
|
|
307
|
+
if (!afterMappings.has(sourceId)) {
|
|
308
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_DRIFT', {
|
|
309
|
+
adapter: current?.adapter,
|
|
310
|
+
expected: canonicalBridgeJson(mapping), observed: '(missing)',
|
|
311
|
+
message: `external capability/change/task mapping disappeared: ${sourceId}`,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
316
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const BRIDGE_DIAGNOSTIC_CODES = Object.freeze({
|
|
2
|
+
adapterDisabled: 'BRIDGE_ADAPTER_DISABLED',
|
|
3
|
+
adapterMissing: 'BRIDGE_ADAPTER_MISSING',
|
|
4
|
+
adapterUnknown: 'BRIDGE_ADAPTER_UNKNOWN',
|
|
5
|
+
versionMissing: 'BRIDGE_VERSION_MISSING',
|
|
6
|
+
versionIncompatible: 'BRIDGE_VERSION_INCOMPATIBLE',
|
|
7
|
+
ownershipConflict: 'BRIDGE_OWNERSHIP_CONFLICT',
|
|
8
|
+
sourceInvalid: 'BRIDGE_SOURCE_INVALID',
|
|
9
|
+
sourceDrift: 'BRIDGE_SOURCE_DRIFT',
|
|
10
|
+
sourceIdDuplicate: 'BRIDGE_SOURCE_ID_DUPLICATE',
|
|
11
|
+
contractInvalid: 'BRIDGE_CONTRACT_INVALID',
|
|
12
|
+
contractStale: 'BRIDGE_CONTRACT_STALE',
|
|
13
|
+
canonicalAuthorityRequired: 'BRIDGE_CANONICAL_AUTHORITY_REQUIRED',
|
|
14
|
+
projectionInvalid: 'BRIDGE_PROJECTION_INVALID',
|
|
15
|
+
schemaInvalid: 'BRIDGE_SCHEMA_INVALID',
|
|
16
|
+
baselineMissing: 'BRIDGE_BASELINE_MISSING',
|
|
17
|
+
baselineStale: 'BRIDGE_BASELINE_STALE',
|
|
18
|
+
baselineInvalid: 'BRIDGE_BASELINE_INVALID',
|
|
19
|
+
artifactManifestInvalid: 'BRIDGE_ARTIFACT_MANIFEST_INVALID',
|
|
20
|
+
artifactManifestUntracked: 'BRIDGE_ARTIFACT_MANIFEST_UNTRACKED',
|
|
21
|
+
artifactForged: 'BRIDGE_ARTIFACT_FORGED',
|
|
22
|
+
artifactTaskUnbound: 'BRIDGE_ARTIFACT_TASK_UNBOUND',
|
|
23
|
+
artifactResultMissing: 'BRIDGE_ARTIFACT_RESULT_MISSING',
|
|
24
|
+
proofMissing: 'BRIDGE_PROOF_MISSING',
|
|
25
|
+
proofUnverified: 'BRIDGE_PROOF_UNVERIFIED',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function bridgeDiagnostic(code, {
|
|
29
|
+
adapter = '', message = '', blocking = true, expected = '', observed = '', path = '',
|
|
30
|
+
} = {}) {
|
|
31
|
+
return {
|
|
32
|
+
schema_version: 1,
|
|
33
|
+
code: String(code),
|
|
34
|
+
adapter: String(adapter),
|
|
35
|
+
blocking: Boolean(blocking),
|
|
36
|
+
message: String(message || code),
|
|
37
|
+
...(expected ? { expected: String(expected) } : {}),
|
|
38
|
+
...(observed ? { observed: String(observed) } : {}),
|
|
39
|
+
...(path ? { path: String(path).replaceAll('\\', '/') } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function bridgeError(code, message, details = {}) {
|
|
44
|
+
return Object.assign(new Error(message), { code, diagnostics: [bridgeDiagnostic(code, details)] });
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const dispatchReceipts = new WeakMap();
|
|
2
|
+
const artifactProofReceipts = new WeakMap();
|
|
3
|
+
|
|
4
|
+
function receipt(kind) {
|
|
5
|
+
return Object.freeze({ schema_version: 1, kind });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Internal composition-root capability. This module is deliberately not re-exported by index.mjs.
|
|
9
|
+
export function issueCanonicalDispatchAuthority({ task_contract, active_context } = {}) {
|
|
10
|
+
const value = receipt('canonical-dispatch-authority');
|
|
11
|
+
dispatchReceipts.set(value, {
|
|
12
|
+
task_contract: structuredClone(task_contract),
|
|
13
|
+
active_context: structuredClone(active_context),
|
|
14
|
+
});
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readCanonicalDispatchAuthority(value) {
|
|
19
|
+
const payload = value && typeof value === 'object' ? dispatchReceipts.get(value) : null;
|
|
20
|
+
return payload ? structuredClone(payload) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function issueCanonicalArtifactProof(proof = {}) {
|
|
24
|
+
const value = receipt('canonical-artifact-proof');
|
|
25
|
+
artifactProofReceipts.set(value, structuredClone(proof));
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readCanonicalArtifactProof(value) {
|
|
30
|
+
const payload = value && typeof value === 'object' ? artifactProofReceipts.get(value) : null;
|
|
31
|
+
return payload ? structuredClone(payload) : null;
|
|
32
|
+
}
|
|
@@ -7,6 +7,40 @@ export const HOST_CAPABILITIES = [
|
|
|
7
7
|
|
|
8
8
|
export const HOST_CAPABILITY_STATES = ['native', 'adapted', 'polled', 'manual', 'unavailable'];
|
|
9
9
|
|
|
10
|
+
export const ECOSYSTEM_ADAPTER_MANIFESTS = Object.freeze({
|
|
11
|
+
'spec-kit': Object.freeze({
|
|
12
|
+
adapter_id: 'spec-kit', contract_version: 1, compatibility_range: '>=0.1.0 <2.0.0', optional: true,
|
|
13
|
+
}),
|
|
14
|
+
superpowers: Object.freeze({
|
|
15
|
+
adapter_id: 'superpowers', contract_version: 1, compatibility_range: '>=1.0.0 <2.0.0', optional: true,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function semverTuple(value) {
|
|
20
|
+
const match = String(value || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
21
|
+
return match ? match.slice(1).map(Number) : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function compareVersion(left, right) {
|
|
25
|
+
for (let index = 0; index < 3; index += 1) {
|
|
26
|
+
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isVersionInRange(version, range) {
|
|
32
|
+
const observed = semverTuple(version);
|
|
33
|
+
if (!observed) return false;
|
|
34
|
+
const clauses = String(range || '').split(/\s+/).filter(Boolean);
|
|
35
|
+
if (!clauses.length) return false;
|
|
36
|
+
return clauses.every((clause) => {
|
|
37
|
+
const match = clause.match(/^(>=|>|<=|<|=)?(\d+\.\d+\.\d+)$/);
|
|
38
|
+
if (!match) return false;
|
|
39
|
+
const comparison = compareVersion(observed, semverTuple(match[2]));
|
|
40
|
+
return ({ '>=': comparison >= 0, '>': comparison > 0, '<=': comparison <= 0, '<': comparison < 0, '=': comparison === 0 }[match[1] || '=']);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
function matrix(defaultState, overrides = {}) {
|
|
11
45
|
return Object.fromEntries(HOST_CAPABILITIES.map((capability) => [
|
|
12
46
|
capability, overrides[capability] || defaultState,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { assessBridgeAdapter, inspectBridgeAdapterRoot, readBridgeConfig } from './bridge-config.mjs';
|
|
4
|
+
import { validateBridgeOwnership } from './bridge-contract.mjs';
|
|
5
|
+
import { detectSpecKit } from './spec-kit-adapter.mjs';
|
|
6
|
+
|
|
7
|
+
export function inspectEcosystemBridges({ projectRoot, configPath = '', fs = null } = {}) {
|
|
8
|
+
try {
|
|
9
|
+
const root = resolve(projectRoot || process.cwd());
|
|
10
|
+
const loaded = readBridgeConfig(root, configPath, { fs });
|
|
11
|
+
const spec = detectSpecKit({ projectRoot: root, config: loaded.config, fs });
|
|
12
|
+
const specOwnership = validateBridgeOwnership({
|
|
13
|
+
adapter: 'spec-kit', claims: loaded.config.adapters['spec-kit'].ownership_claims || [],
|
|
14
|
+
});
|
|
15
|
+
const superConfig = loaded.config.adapters.superpowers;
|
|
16
|
+
const superRoot = inspectBridgeAdapterRoot(root, superConfig.root || '.superpowers', { adapter: 'superpowers', fs });
|
|
17
|
+
const superAssessment = !superRoot.valid
|
|
18
|
+
? {
|
|
19
|
+
available: false,
|
|
20
|
+
diagnostics: superRoot.diagnostics,
|
|
21
|
+
}
|
|
22
|
+
: assessBridgeAdapter('superpowers', {
|
|
23
|
+
config: loaded.config,
|
|
24
|
+
detectedVersion: superConfig.version || '',
|
|
25
|
+
present: superRoot.present,
|
|
26
|
+
});
|
|
27
|
+
const superOwnership = validateBridgeOwnership({
|
|
28
|
+
adapter: 'superpowers', claims: superConfig.ownership_claims || [],
|
|
29
|
+
});
|
|
30
|
+
const adapters = [
|
|
31
|
+
{
|
|
32
|
+
adapter: 'spec-kit',
|
|
33
|
+
active: spec.assessment.available,
|
|
34
|
+
available: spec.assessment.available,
|
|
35
|
+
version: spec.version,
|
|
36
|
+
diagnostics: [...spec.assessment.diagnostics, ...(loaded.config.adapters['spec-kit'].enabled ? specOwnership.diagnostics : [])],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
adapter: 'superpowers',
|
|
40
|
+
active: superAssessment.available,
|
|
41
|
+
available: superAssessment.available,
|
|
42
|
+
version: superAssessment.version || superConfig.version || '',
|
|
43
|
+
diagnostics: [...superAssessment.diagnostics, ...(superConfig.enabled ? superOwnership.diagnostics : [])],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const diagnostics = adapters.flatMap((item) => item.diagnostics);
|
|
47
|
+
return {
|
|
48
|
+
schema_version: 1,
|
|
49
|
+
ok: diagnostics.every((item) => !item.blocking),
|
|
50
|
+
config_path: loaded.path,
|
|
51
|
+
config_exists: loaded.exists,
|
|
52
|
+
adapters,
|
|
53
|
+
diagnostics,
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
schema_version: 1,
|
|
58
|
+
ok: false,
|
|
59
|
+
config_path: '',
|
|
60
|
+
config_exists: false,
|
|
61
|
+
adapters: [],
|
|
62
|
+
diagnostics: error?.diagnostics || [{
|
|
63
|
+
schema_version: 1,
|
|
64
|
+
code: error?.code || 'BRIDGE_CONFIG_INVALID',
|
|
65
|
+
adapter: '',
|
|
66
|
+
blocking: true,
|
|
67
|
+
message: error?.message || String(error),
|
|
68
|
+
}],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function renderEcosystemBridgeLines(result) {
|
|
74
|
+
const lines = [`[bridges] ${result.ok ? 'saudável' : 'bloqueado'} · ${result.adapters.filter((item) => item.active).length} ativo(s)`];
|
|
75
|
+
for (const adapter of result.adapters) {
|
|
76
|
+
lines.push(` ${adapter.active ? '✓' : '·'} ${adapter.adapter}: ${adapter.active ? `compatível ${adapter.version}` : adapter.diagnostics[0]?.code || 'inativo'}`);
|
|
77
|
+
}
|
|
78
|
+
for (const diagnostic of result.diagnostics.filter((item) => item.blocking)) {
|
|
79
|
+
lines.push(` ✗ ${diagnostic.code}: ${diagnostic.message}`);
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
@@ -5,3 +5,9 @@ export * from './prompt-content.mjs';
|
|
|
5
5
|
export * from './transcript-usage.mjs';
|
|
6
6
|
export * from './transcripts.mjs';
|
|
7
7
|
export * from './session-identity.mjs';
|
|
8
|
+
export * from './bridge-contract.mjs';
|
|
9
|
+
export * from './bridge-diagnostics.mjs';
|
|
10
|
+
export * from './bridge-config.mjs';
|
|
11
|
+
export * from './spec-kit-adapter.mjs';
|
|
12
|
+
export * from './superpowers-adapter.mjs';
|
|
13
|
+
export * from './ecosystem-bridge.mjs';
|