unitbob 0.1.9 → 0.1.11
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/README.md +14 -7
- package/dist/files/mapBuild.js +31 -2
- package/dist/proc.js +59 -9
- package/dist/verbs/mapPrepare.js +16 -4
- package/dist/verbs/putMapBuild.js +13 -3
- package/dist/wire.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,13 +31,20 @@ Restart the session so the commands load.
|
|
|
31
31
|
|
|
32
32
|
## Full cycle
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
34
|
+
Just type it in the chat. There is nothing to memorise and no command to get right.
|
|
35
|
+
|
|
36
|
+
| Step | Say this |
|
|
37
|
+
|------|----------|
|
|
38
|
+
| 1. Build the map | `Build my Unitbob map` |
|
|
39
|
+
| 2. Generate tests | `Generate the guardrail tests` |
|
|
40
|
+
| 3. Run the checks | `Run the checks` |
|
|
41
|
+
| 4. Fix a red lamp | `Fix guardrail <id>` |
|
|
42
|
+
| 5. Open the map | `Open my Unitbob map` |
|
|
43
|
+
|
|
44
|
+
There are also `/unitbob:map`, `/unitbob:suite` and friends, but they work only
|
|
45
|
+
inside a Claude Code terminal, and only in a session started after the plugin was
|
|
46
|
+
installed — in a browser or desktop window they are not recognised at all. The
|
|
47
|
+
phrasings above work everywhere, so they are the ones documented here.
|
|
41
48
|
|
|
42
49
|
---
|
|
43
50
|
|
package/dist/files/mapBuild.js
CHANGED
|
@@ -11,6 +11,14 @@ export function requestPath(projectRoot) {
|
|
|
11
11
|
export function outputPath(projectRoot) {
|
|
12
12
|
return join(projectRoot, '.unitbob', 'map-build', 'map_document.json');
|
|
13
13
|
}
|
|
14
|
+
// The surface inventory (spec 31, step 1) and the grouped surface document
|
|
15
|
+
// (step 2) are the two extra local artifacts of one atomic map build.
|
|
16
|
+
export function surfacesPath(projectRoot) {
|
|
17
|
+
return join(projectRoot, '.unitbob', 'map-build', 'surfaces.json');
|
|
18
|
+
}
|
|
19
|
+
export function surfaceOutputPath(projectRoot) {
|
|
20
|
+
return join(projectRoot, '.unitbob', 'map-build', 'surface_document.json');
|
|
21
|
+
}
|
|
14
22
|
export function readFreshGraph(projectRoot) {
|
|
15
23
|
const path = graphPath(projectRoot);
|
|
16
24
|
if (!existsSync(path)) {
|
|
@@ -25,6 +33,8 @@ export function writeMapBuildRequest(projectRoot, recipes) {
|
|
|
25
33
|
project_root: projectRoot,
|
|
26
34
|
graph_path: graphPath(projectRoot),
|
|
27
35
|
output_path: outputPath(projectRoot),
|
|
36
|
+
surfaces_path: surfacesPath(projectRoot),
|
|
37
|
+
surface_output_path: surfaceOutputPath(projectRoot),
|
|
28
38
|
recipes,
|
|
29
39
|
};
|
|
30
40
|
const path = requestPath(projectRoot);
|
|
@@ -39,7 +49,8 @@ export function readMapBuildRequest(projectRoot) {
|
|
|
39
49
|
}
|
|
40
50
|
const packet = parseJson(readFileSync(path, 'utf8'), path);
|
|
41
51
|
if (!isMapBuildRequest(packet)) {
|
|
42
|
-
throw new Error(`${path} is malformed: expected project_root, graph_path, output_path,
|
|
52
|
+
throw new Error(`${path} is malformed: expected project_root, graph_path, output_path, surfaces_path, ` +
|
|
53
|
+
'surface_output_path, and recipes.');
|
|
43
54
|
}
|
|
44
55
|
return packet;
|
|
45
56
|
}
|
|
@@ -49,6 +60,20 @@ export function readHostMapOutput(path) {
|
|
|
49
60
|
}
|
|
50
61
|
return parseJson(readFileSync(path, 'utf8'), path);
|
|
51
62
|
}
|
|
63
|
+
// The two surface artifacts are both required before upload: no partial bundle
|
|
64
|
+
// carrying only one lens is ever sent (spec 31, atomic bundle).
|
|
65
|
+
export function readSurfacesInventory(path) {
|
|
66
|
+
if (!existsSync(path)) {
|
|
67
|
+
throw new Error(`${path} not found — the extract_surfaces step did not write a surfaces inventory.`);
|
|
68
|
+
}
|
|
69
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
70
|
+
}
|
|
71
|
+
export function readSurfaceDocument(path) {
|
|
72
|
+
if (!existsSync(path)) {
|
|
73
|
+
throw new Error(`${path} not found — the decompose_surfaces step did not write a surface document.`);
|
|
74
|
+
}
|
|
75
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
76
|
+
}
|
|
52
77
|
function parseJson(raw, path) {
|
|
53
78
|
try {
|
|
54
79
|
return JSON.parse(raw);
|
|
@@ -65,9 +90,13 @@ function isMapBuildRequest(value) {
|
|
|
65
90
|
return (typeof packet.project_root === 'string' &&
|
|
66
91
|
typeof packet.graph_path === 'string' &&
|
|
67
92
|
typeof packet.output_path === 'string' &&
|
|
93
|
+
typeof packet.surfaces_path === 'string' &&
|
|
94
|
+
typeof packet.surface_output_path === 'string' &&
|
|
68
95
|
!!recipes &&
|
|
69
96
|
isRecipe(recipes.decompose) &&
|
|
70
|
-
isRecipe(recipes.relate)
|
|
97
|
+
isRecipe(recipes.relate) &&
|
|
98
|
+
isRecipe(recipes.extract_surfaces) &&
|
|
99
|
+
isRecipe(recipes.decompose_surfaces));
|
|
71
100
|
}
|
|
72
101
|
function isRecipe(value) {
|
|
73
102
|
if (!value || typeof value !== 'object')
|
package/dist/proc.js
CHANGED
|
@@ -56,20 +56,70 @@ export async function requireGraphify() {
|
|
|
56
56
|
`"graphify", needs Python 3.10+), then retry (${err.message}).`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
// Paths that hold no business logic in the stacks unitbob supports — Rails,
|
|
60
|
+
// JS/TS, Python — and that graphify does not already skip (it drops
|
|
61
|
+
// node_modules, venv, dist, build, target, out, __pycache__, and the framework
|
|
62
|
+
// cache and report dirs on its own).
|
|
63
|
+
//
|
|
64
|
+
// Every entry earns its place, because an over-broad pattern fails silently: a
|
|
65
|
+
// subsystem simply never appears on the map and nobody learns why. Measured on
|
|
66
|
+
// one real Rails app (2943 nodes), vendor/ was 1337 of them, app/assets/ 414,
|
|
67
|
+
// db/migrate/ 314 — 70% of the graph, and the busiest nodes in it were `_()`
|
|
68
|
+
// and `$()`. Everything else in that project was already clean: bin/, tmp/,
|
|
69
|
+
// log/ and storage/ produced zero nodes, so they are deliberately not listed.
|
|
70
|
+
//
|
|
71
|
+
// The list stays language-neutral: one project is often Rails and Python at
|
|
72
|
+
// once. Gitignore syntax; graphify merges it with .gitignore, and it can only
|
|
73
|
+
// ever exclude more, never re-include.
|
|
74
|
+
export const GRAPH_NOISE_PATTERNS = [
|
|
75
|
+
'# unitbob: keep the graph about your own business code',
|
|
76
|
+
'.unitbob/',
|
|
77
|
+
// Third-party code committed into the repo. In Rails, `vendor/` is the
|
|
78
|
+
// convention for it, and `app/assets/javascripts/` is where sprockets-era
|
|
79
|
+
// apps dumped libraries — on the measured app that folder was moment.js,
|
|
80
|
+
// datatables.js and jquery.inputmask, against a single node of own code. A
|
|
81
|
+
// modern Rails app keeps its own JS in `app/javascript/`, which stays.
|
|
82
|
+
'vendor/',
|
|
83
|
+
'app/assets/javascripts/',
|
|
84
|
+
'app/assets/builds/',
|
|
85
|
+
'app/assets/config/',
|
|
86
|
+
'public/assets/',
|
|
87
|
+
'public/packs/',
|
|
88
|
+
'*.min.js',
|
|
89
|
+
'*.min.css',
|
|
90
|
+
'*.bundle.js',
|
|
91
|
+
// Generated code: schema history and codegen output. Describes the shape of
|
|
92
|
+
// data, never the behaviour a guardrail could protect.
|
|
93
|
+
'db/migrate/',
|
|
94
|
+
'db/schema.rb',
|
|
95
|
+
'db/structure.sql',
|
|
96
|
+
'migrations/',
|
|
97
|
+
'__generated__/',
|
|
98
|
+
'*_pb2.py',
|
|
99
|
+
'*_pb2_grpc.py',
|
|
100
|
+
'*_pb.js',
|
|
101
|
+
// Type declarations — a contract for a compiler, with no runtime behaviour.
|
|
102
|
+
'*.d.ts',
|
|
103
|
+
'*.pyi',
|
|
104
|
+
];
|
|
59
105
|
export function ensureUnitbobIgnored(projectRoot) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
// `.graphifyignore` is unitbob's own bookkeeping, like the other two entries —
|
|
107
|
+
// the user never edits it, so it stays out of their commits.
|
|
108
|
+
ensureLines(join(projectRoot, '.gitignore'), ['.unitbob/', 'graphify-out/', '.graphifyignore']);
|
|
109
|
+
ensureLines(join(projectRoot, '.graphifyignore'), GRAPH_NOISE_PATTERNS);
|
|
63
110
|
}
|
|
64
|
-
|
|
111
|
+
// Appends whichever lines are missing, in one write, leaving the user's own
|
|
112
|
+
// entries (and their order) untouched. Idempotent: a second run adds nothing.
|
|
113
|
+
function ensureLines(path, lines) {
|
|
65
114
|
let current = '';
|
|
66
|
-
if (existsSync(path))
|
|
115
|
+
if (existsSync(path))
|
|
67
116
|
current = readFileSync(path, 'utf8');
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
const present = new Set(current.split('\n').map((line) => line.trim()));
|
|
118
|
+
const missing = lines.filter((line) => !present.has(line));
|
|
119
|
+
if (missing.length === 0)
|
|
120
|
+
return;
|
|
71
121
|
const prefix = current.length > 0 && !current.endsWith('\n') ? '\n' : '';
|
|
72
|
-
writeFileSync(path, `${current}${prefix}${
|
|
122
|
+
writeFileSync(path, `${current}${prefix}${missing.join('\n')}\n`);
|
|
73
123
|
}
|
|
74
124
|
export async function runGraphifyExtractKeyless(projectRoot) {
|
|
75
125
|
// Deterministic AST-only graph; no LLM, no API key. `update --force` re-extracts
|
package/dist/verbs/mapPrepare.js
CHANGED
|
@@ -20,9 +20,21 @@ export async function mapPrepare(config, _args = [], deps) {
|
|
|
20
20
|
throw new Error(`graphify update failed: ${detail}`);
|
|
21
21
|
}
|
|
22
22
|
readFreshGraph(config.projectRoot);
|
|
23
|
-
const [decompose, relate] = await Promise.all([
|
|
24
|
-
|
|
23
|
+
const [decompose, relate, extractSurfaces, decomposeSurfaces] = await Promise.all([
|
|
24
|
+
actual.getRecipe('decompose'),
|
|
25
|
+
actual.getRecipe('relate'),
|
|
26
|
+
actual.getRecipe('extract_surfaces'),
|
|
27
|
+
actual.getRecipe('decompose_surfaces'),
|
|
28
|
+
]);
|
|
29
|
+
const packet = writeMapBuildRequest(config.projectRoot, {
|
|
30
|
+
decompose,
|
|
31
|
+
relate,
|
|
32
|
+
extract_surfaces: extractSurfaces,
|
|
33
|
+
decompose_surfaces: decomposeSurfaces,
|
|
34
|
+
});
|
|
25
35
|
process.stdout.write(`Map build request written to ${packet.project_root}/.unitbob/map-build/request.json\n`);
|
|
26
|
-
process.stdout.write(`Next: build the
|
|
27
|
-
|
|
36
|
+
process.stdout.write(`Next: build BOTH lenses following the recipes in that request — the decompose map at ` +
|
|
37
|
+
`${packet.output_path} (recipes.decompose, recipes.relate), and the surface map at ` +
|
|
38
|
+
`${packet.surface_output_path} (recipes.extract_surfaces → ${packet.surfaces_path}, then ` +
|
|
39
|
+
'recipes.decompose_surfaces) — then run `unitbob put-map-build`.\n');
|
|
28
40
|
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { readHostMapOutput, readMapBuildRequest } from "../files/mapBuild.js";
|
|
2
|
+
import { readHostMapOutput, readMapBuildRequest, readSurfaceDocument, readSurfacesInventory, } from "../files/mapBuild.js";
|
|
3
3
|
import { Wire } from "../wire.js";
|
|
4
4
|
export async function putMapBuild(config, _args = [], deps) {
|
|
5
5
|
const packet = readMapBuildRequest(config.projectRoot);
|
|
6
6
|
const graph = JSON.parse(readFileSync(packet.graph_path, 'utf8'));
|
|
7
|
+
// Both lenses must be present locally before anything is sent — the host stores
|
|
8
|
+
// the bundle atomically or not at all (spec 31). A missing artifact here throws
|
|
9
|
+
// and no partial upload happens.
|
|
7
10
|
const mapDocument = readHostMapOutput(packet.output_path);
|
|
11
|
+
const surfaces = readSurfacesInventory(packet.surfaces_path);
|
|
12
|
+
const surfaceDocument = readSurfaceDocument(packet.surface_output_path);
|
|
8
13
|
const actual = {
|
|
9
14
|
putMapBuild: (payload) => new Wire(config).putMapBuild(payload),
|
|
10
15
|
...deps,
|
|
11
16
|
};
|
|
12
|
-
const result = await actual.putMapBuild({
|
|
13
|
-
|
|
17
|
+
const result = await actual.putMapBuild({
|
|
18
|
+
graph,
|
|
19
|
+
map_document: mapDocument,
|
|
20
|
+
surfaces,
|
|
21
|
+
surface_document: surfaceDocument,
|
|
22
|
+
});
|
|
23
|
+
process.stdout.write(`Map uploaded (map ${result.map_digest}, surface ${result.surface_digest}, graph ${result.graph_digest}) ` +
|
|
14
24
|
`${result.reused ? 'reused' : 'created'} version ${result.map_version_id}.\n` +
|
|
15
25
|
`${result.map_url}\n`);
|
|
16
26
|
}
|
package/dist/wire.js
CHANGED
|
@@ -40,8 +40,10 @@ export class Wire {
|
|
|
40
40
|
constructor(config) {
|
|
41
41
|
this.config = config;
|
|
42
42
|
}
|
|
43
|
-
// PUT /repos/:id/map_build — upload the fresh graph and host-built
|
|
44
|
-
//
|
|
43
|
+
// PUT /repos/:id/map_build — upload the fresh graph and both host-built lenses
|
|
44
|
+
// (decompose map_document + surfaces inventory + grouped surface_document) as
|
|
45
|
+
// one atomic bundle. Rails validates both documents, versions, and computes all
|
|
46
|
+
// digests; either lens failing rejects the whole bundle (spec 31).
|
|
45
47
|
async putMapBuild(payload) {
|
|
46
48
|
const res = await this.send('PUT', this.repoPath('map_build'), payload);
|
|
47
49
|
await this.ensureOk(res, `PUT ${this.repoPath('map_build')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|