@skanl/brambo-environment 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/doctor.d.ts +261 -0
- package/dist/doctor.js +551 -0
- package/dist/executors.d.ts +88 -0
- package/dist/executors.js +109 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +60 -0
- package/dist/ingest.d.ts +60 -0
- package/dist/ingest.js +85 -0
- package/dist/init.d.ts +325 -0
- package/dist/init.js +641 -0
- package/dist/remediate.d.ts +51 -0
- package/dist/remediate.js +140 -0
- package/package.json +56 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RemediationKind, RemediationOutcome, RemediationRefusal } from '@skanl/brambo-contracts';
|
|
2
|
+
import type { ProjectionMode } from '@skanl/brambo-projection';
|
|
3
|
+
import type { Diagnosis, DiagnosisFinding } from './doctor.ts';
|
|
4
|
+
export interface RemediateOptions {
|
|
5
|
+
/** Defaults to the OS home directory. */
|
|
6
|
+
readonly homeDir?: string;
|
|
7
|
+
/** Read only for the project scope, where it defaults to `process.cwd()`. */
|
|
8
|
+
readonly projectDir?: string;
|
|
9
|
+
/** Defaults to `'machine'`, mirroring `brambo init` and `brambo doctor`. */
|
|
10
|
+
readonly scope?: 'machine' | 'project';
|
|
11
|
+
readonly remediation: RemediationKind;
|
|
12
|
+
/** Narrows the finding; required whenever more than one would match. */
|
|
13
|
+
readonly executorId?: string;
|
|
14
|
+
readonly entryId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Defaults to `'inspect'` — the OPPOSITE default from `runProjection`, and
|
|
17
|
+
* deliberately so. A projection converges a machine a user asked brambo to
|
|
18
|
+
* manage; a remediation changes who owns what, and describing it first is the
|
|
19
|
+
* frozen requirement. A caller that wants the act asks for `'apply'`.
|
|
20
|
+
*/
|
|
21
|
+
readonly mode?: ProjectionMode;
|
|
22
|
+
}
|
|
23
|
+
export interface RemediationReport {
|
|
24
|
+
readonly scope: 'machine' | 'project';
|
|
25
|
+
readonly remediation: RemediationKind;
|
|
26
|
+
readonly mode: ProjectionMode;
|
|
27
|
+
/** The finding acted on, exactly as `brambo doctor` reports it. */
|
|
28
|
+
readonly finding?: DiagnosisFinding;
|
|
29
|
+
readonly outcome?: RemediationOutcome;
|
|
30
|
+
/** Why brambo did not select a finding to act on. */
|
|
31
|
+
readonly refusal?: RemediationRefusal;
|
|
32
|
+
/**
|
|
33
|
+
* Every finding this remediation is the exit for, in this run. Present
|
|
34
|
+
* whenever the request matched none or more than one, because "name one of
|
|
35
|
+
* these" is only actionable if the user can see them.
|
|
36
|
+
*/
|
|
37
|
+
readonly candidates: readonly DiagnosisFinding[];
|
|
38
|
+
/** The full diagnosis the selection was made from; nothing here is invented. */
|
|
39
|
+
readonly diagnosis: Diagnosis;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Describes — or, with `mode: 'apply'`, performs — exactly one remediation for
|
|
43
|
+
* exactly one finding this run reported.
|
|
44
|
+
*
|
|
45
|
+
* Nothing else is touched. `adopt` and `release` change only what brambo claims;
|
|
46
|
+
* `repair` rewrites only brambo's own ledger; `discard` removes only brambo's own
|
|
47
|
+
* prior output from one vendor file. No other entry, no other finding and no
|
|
48
|
+
* foreign neighbour is read or written, which is what makes "nothing unnamed is
|
|
49
|
+
* touched" a property of the design rather than a promise about the code.
|
|
50
|
+
*/
|
|
51
|
+
export declare function remediate(options: RemediateOptions): Promise<RemediationReport>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { BRAMBO_ERROR_CODES, projectionTargetLocation } from '@skanl/brambo-contracts';
|
|
3
|
+
import { ProjectionLedger, groupByKind, runRemediation } from '@skanl/brambo-projection';
|
|
4
|
+
// The exit table lives in `doctor.ts`, beside the finding kinds it is total
|
|
5
|
+
// over, and this file only ASKS it which kinds a verb resolves. Owning it here
|
|
6
|
+
// is what shipped the first time, and the consequence was a product that printed
|
|
7
|
+
// a terminal resolution for four states it could actually leave: the report and
|
|
8
|
+
// the exit have to be one table.
|
|
9
|
+
import { diagnose, findingKindsFor } from './doctor.js';
|
|
10
|
+
import { EXECUTOR_PROFILES } from './executors.js';
|
|
11
|
+
import { scopeDirectory, storeFor, targetsFor } from './init.js';
|
|
12
|
+
function refusalOf(message) {
|
|
13
|
+
return { code: BRAMBO_ERROR_CODES.projectionRemediationRefused, message };
|
|
14
|
+
}
|
|
15
|
+
function describeFinding(found) {
|
|
16
|
+
const about = [found.executorId, found.entryId, found.location, found.filePath].filter((part) => part !== undefined);
|
|
17
|
+
return `${found.kind}${about.length === 0 ? '' : ` (${about.join(' · ')})`}`;
|
|
18
|
+
}
|
|
19
|
+
/** The target whose location a finding is about, out of the ones this scope runs. */
|
|
20
|
+
function targetFor(finding, planned) {
|
|
21
|
+
return planned.find((candidate) => candidate.profile.executorId === finding.executorId &&
|
|
22
|
+
projectionTargetLocation(candidate.target) === finding.filePath)?.target;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Describes — or, with `mode: 'apply'`, performs — exactly one remediation for
|
|
26
|
+
* exactly one finding this run reported.
|
|
27
|
+
*
|
|
28
|
+
* Nothing else is touched. `adopt` and `release` change only what brambo claims;
|
|
29
|
+
* `repair` rewrites only brambo's own ledger; `discard` removes only brambo's own
|
|
30
|
+
* prior output from one vendor file. No other entry, no other finding and no
|
|
31
|
+
* foreign neighbour is read or written, which is what makes "nothing unnamed is
|
|
32
|
+
* touched" a property of the design rather than a promise about the code.
|
|
33
|
+
*/
|
|
34
|
+
export async function remediate(options) {
|
|
35
|
+
// Every caller-controlled field read ONCE, before the first await — the same
|
|
36
|
+
// TOCTOU rule `initMachine` and `diagnose` follow, and it decides here which
|
|
37
|
+
// machine gets written to.
|
|
38
|
+
const { homeDir = homedir(), projectDir, scope = 'machine', remediation, executorId, entryId, mode = 'inspect', } = options;
|
|
39
|
+
const home = await scopeDirectory('the home directory', homeDir);
|
|
40
|
+
const root = scope === 'machine'
|
|
41
|
+
? home
|
|
42
|
+
: await scopeDirectory('the project directory', projectDir ?? process.cwd());
|
|
43
|
+
const diagnosis = await diagnose({ homeDir: home, projectDir: root, scope });
|
|
44
|
+
const base = { scope, remediation, mode, diagnosis };
|
|
45
|
+
const kinds = findingKindsFor(remediation);
|
|
46
|
+
if (kinds.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
...base,
|
|
49
|
+
candidates: [],
|
|
50
|
+
refusal: refusalOf(`'${remediation}' is not the exit for any state brambo reports, so there is nothing it could be asked to do`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const candidates = diagnosis.findings.filter((found) => kinds.includes(found.kind));
|
|
54
|
+
const selected = candidates.filter((found) => (executorId === undefined || found.executorId === executorId) &&
|
|
55
|
+
(entryId === undefined || found.entryId === entryId));
|
|
56
|
+
if (selected.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
...base,
|
|
59
|
+
candidates,
|
|
60
|
+
refusal: refusalOf(candidates.length === 0
|
|
61
|
+
? `brambo reported no ${kinds.join(' or ')} finding in this run, so there is nothing for '${remediation}' to resolve; brambo never remediates a state it did not just report`
|
|
62
|
+
: `no ${kinds.join(' or ')} finding in this run matches ${JSON.stringify({ executorId, entryId })}; brambo reported ${candidates.map(describeFinding).join(', ')}`),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (selected.length > 1) {
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
candidates: selected,
|
|
69
|
+
refusal: refusalOf(`'${remediation}' resolves one finding at a time and ${selected.length} match: ${selected
|
|
70
|
+
.map(describeFinding)
|
|
71
|
+
.join(', ')}. Name one with --executor and --entry`),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const finding = selected[0];
|
|
75
|
+
const ledger = new ProjectionLedger({ homeDir: home });
|
|
76
|
+
if (remediation === 'repair') {
|
|
77
|
+
return { ...base, finding, candidates: [], outcome: await runRemediation({ remediation, ledger, mode }) };
|
|
78
|
+
}
|
|
79
|
+
if (remediation === 'discard') {
|
|
80
|
+
const profile = EXECUTOR_PROFILES.find((candidate) => candidate.executorId === finding.executorId);
|
|
81
|
+
const location = profile?.legacyConfig?.(home);
|
|
82
|
+
if (profile === undefined || location === undefined) {
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
finding,
|
|
86
|
+
candidates: [],
|
|
87
|
+
refusal: refusalOf(`brambo knows no location where a previous build could have written into '${finding.executorId ?? 'an unnamed executor'}'`),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...base,
|
|
92
|
+
finding,
|
|
93
|
+
candidates: [],
|
|
94
|
+
// `rootPath: home`, so the containment check in `runRemediation` is against
|
|
95
|
+
// the scope brambo was pointed at rather than against a path this file made
|
|
96
|
+
// up. A legacy location is machine-scoped by measurement (see the profile).
|
|
97
|
+
outcome: await runRemediation({
|
|
98
|
+
remediation,
|
|
99
|
+
legacy: { targetId: profile.targetId, rootPath: home, ...location },
|
|
100
|
+
mode,
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const { planned, skills } = targetsFor(scope, diagnosis.detected, home, root);
|
|
105
|
+
const target = targetFor(finding, [...planned, ...skills]);
|
|
106
|
+
if (target === undefined || finding.entryId === undefined) {
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
finding,
|
|
110
|
+
candidates: [],
|
|
111
|
+
refusal: refusalOf(`brambo could not tie ${describeFinding(finding)} back to one projection target and one registry entry, so it will not act on it`),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (remediation === 'release') {
|
|
115
|
+
return {
|
|
116
|
+
...base,
|
|
117
|
+
finding,
|
|
118
|
+
candidates: [],
|
|
119
|
+
outcome: await runRemediation({ remediation, target, entryId: finding.entryId, ledger, mode }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// `adopt` alone needs the registry: the claim's paths come from the TARGET's
|
|
123
|
+
// own plan of what brambo would write, never from a directory listing, so a
|
|
124
|
+
// file the user put beside brambo's is not swept into a record that later
|
|
125
|
+
// authorises deleting it.
|
|
126
|
+
const store = storeFor(scope, home, root);
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = groupByKind(await store.list());
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await store.dispose();
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
finding,
|
|
137
|
+
candidates: [],
|
|
138
|
+
outcome: await runRemediation({ remediation, target, entryId: finding.entryId, entries, ledger, mode }),
|
|
139
|
+
};
|
|
140
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skanl/brambo-environment",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Executor detection, projection orchestration, and the diagnosis `brambo doctor` prints.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agent",
|
|
7
|
+
"brambo",
|
|
8
|
+
"doctor",
|
|
9
|
+
"diagnostics",
|
|
10
|
+
"environment"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/SKANL/brambo#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/SKANL/brambo/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/SKANL/brambo.git",
|
|
19
|
+
"directory": "packages/environment"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"brambo-source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@skanl/brambo-contracts": "0.1.1",
|
|
38
|
+
"@skanl/brambo-kernel": "0.1.1",
|
|
39
|
+
"@skanl/brambo-projection": "0.1.1",
|
|
40
|
+
"@skanl/brambo-registry": "0.1.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.13.3",
|
|
44
|
+
"typescript": "~7.0.2",
|
|
45
|
+
"vitest": "^4.1.11"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"lint": "eslint .",
|
|
54
|
+
"build": "node ../../scripts/clean-dist.mjs && tsc -p tsconfig.build.json"
|
|
55
|
+
}
|
|
56
|
+
}
|