unitbob 0.1.10 → 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.
@@ -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, and recipes.`);
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')
@@ -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([actual.getRecipe('decompose'), actual.getRecipe('relate')]);
24
- const packet = writeMapBuildRequest(config.projectRoot, { decompose, relate });
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 Map Document at ${packet.output_path} following recipes.decompose and ` +
27
- 'recipes.relate inside that request, then run `unitbob put-map-build`.\n');
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({ graph, map_document: mapDocument });
13
- process.stdout.write(`Map uploaded (${result.map_digest}, graph ${result.graph_digest}) ` +
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 map as one
44
- // atomic blob. Rails validates, versions, and computes all digests.
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.10",
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": {