@vibecompass/vibecompass 0.7.0 → 0.9.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.md +32 -1
- package/package.json +1 -1
- package/src/cli.js +132 -4
- package/src/docs-review.js +70 -32
- package/src/init.js +171 -4
- package/src/project-memory.js +24 -5
- package/src/read-model.js +8 -4
- package/src/scaffold.js +12 -4
- package/src/setup.js +8 -6
- package/src/status.js +16 -0
- package/src/sync-binding.js +307 -0
- package/src/sync.js +31 -24
- package/src/workflows/registry.js +3 -3
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const TARGET_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
2
|
+
|
|
3
|
+
function normalizeBindingFields(source) {
|
|
4
|
+
const apiUrl = typeof source?.api_url === 'string' && source.api_url.trim() !== ''
|
|
5
|
+
? source.api_url
|
|
6
|
+
: null;
|
|
7
|
+
const projectId = typeof source?.project_id === 'string' && source.project_id.trim() !== ''
|
|
8
|
+
? source.project_id
|
|
9
|
+
: null;
|
|
10
|
+
const credentialEnvVar = typeof source?.credential_env_var === 'string' && source.credential_env_var.trim() !== ''
|
|
11
|
+
? source.credential_env_var
|
|
12
|
+
: null;
|
|
13
|
+
|
|
14
|
+
if (!apiUrl || !projectId || !credentialEnvVar) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { apiUrl, projectId, credentialEnvVar };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function assertValidSyncTargetName(name) {
|
|
22
|
+
if (typeof name !== 'string' || !TARGET_NAME_PATTERN.test(name)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Sync target name "${name}" is invalid. Use lowercase letters, digits, hyphens, or underscores (e.g. dev, prod).`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readSyncTargets(projectConfig) {
|
|
31
|
+
const sync = projectConfig?.sync;
|
|
32
|
+
if (!sync || typeof sync !== 'object') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const targets = sync.targets && typeof sync.targets === 'object' ? sync.targets : null;
|
|
37
|
+
if (!targets || Object.keys(targets).length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const defaultTarget = typeof sync.default_target === 'string' && targets[sync.default_target]
|
|
42
|
+
? sync.default_target
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
return { defaultTarget, targets };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the effective hosted sync binding for a command (D-236).
|
|
50
|
+
*
|
|
51
|
+
* Resolution order: explicit target name -> sync.default_target -> legacy
|
|
52
|
+
* flat sync fields. Returns null when no complete binding exists. Throws on
|
|
53
|
+
* an explicitly requested target that is unknown or incomplete, because a
|
|
54
|
+
* typo must never silently fall back to a different environment.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveSyncBinding(projectConfig, targetName) {
|
|
57
|
+
const sync = projectConfig?.sync;
|
|
58
|
+
if (!sync || typeof sync !== 'object') {
|
|
59
|
+
if (targetName) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`--sync-target ${targetName} requires a sync section in project.yaml. Run "vibecompass connect-hosted --target ${targetName}" first.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const named = readSyncTargets(projectConfig);
|
|
68
|
+
|
|
69
|
+
if (targetName) {
|
|
70
|
+
const target = named?.targets?.[targetName];
|
|
71
|
+
if (!target || typeof target !== 'object') {
|
|
72
|
+
const available = named ? Object.keys(named.targets) : [];
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unknown sync target "${targetName}". ${
|
|
75
|
+
available.length > 0
|
|
76
|
+
? `Available targets: ${available.join(', ')}.`
|
|
77
|
+
: 'No named sync targets are defined in project.yaml; run "vibecompass connect-hosted --target <name>" to add one.'
|
|
78
|
+
}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fields = normalizeBindingFields(target);
|
|
83
|
+
if (!fields) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Sync target "${targetName}" is incomplete. It must define api_url, project_id, and credential_env_var.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { target: targetName, isDefault: targetName === named.defaultTarget, ...fields };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (named?.defaultTarget) {
|
|
93
|
+
const fields = normalizeBindingFields(named.targets[named.defaultTarget]);
|
|
94
|
+
if (fields) {
|
|
95
|
+
return { target: named.defaultTarget, isDefault: true, ...fields };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const flat = normalizeBindingFields(sync);
|
|
100
|
+
return flat ? { target: null, isDefault: true, ...flat } : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const CURSOR_FIELDS = [
|
|
104
|
+
'last_successful_remote_revision',
|
|
105
|
+
'last_successful_local_root_revision',
|
|
106
|
+
'last_successful_manifest_hash',
|
|
107
|
+
'last_sync_direction',
|
|
108
|
+
'last_sync_at',
|
|
109
|
+
'pending_previews',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
function pickCursorFields(source) {
|
|
113
|
+
const cursor = {};
|
|
114
|
+
for (const field of CURSOR_FIELDS) {
|
|
115
|
+
if (source?.[field] !== undefined) {
|
|
116
|
+
cursor[field] = source[field];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return cursor;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads the sync cursor for the resolved binding (D-237). Legacy flat
|
|
124
|
+
* bindings keep using the flat manifest.sync fields. Named targets read only
|
|
125
|
+
* their own cursor, and only when its recorded api_url/project_id identity
|
|
126
|
+
* matches the current binding — otherwise the cursor is treated as absent so
|
|
127
|
+
* a stale or cross-environment cursor can never become another environment's
|
|
128
|
+
* baseline.
|
|
129
|
+
*/
|
|
130
|
+
export function readSyncCursor(manifestSync, binding) {
|
|
131
|
+
if (!manifestSync || typeof manifestSync !== 'object') {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!binding?.target) {
|
|
136
|
+
return pickCursorFields(manifestSync);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const cursor = manifestSync.targets?.[binding.target];
|
|
140
|
+
if (
|
|
141
|
+
!cursor ||
|
|
142
|
+
typeof cursor !== 'object' ||
|
|
143
|
+
cursor.api_url !== binding.apiUrl ||
|
|
144
|
+
cursor.project_id !== binding.projectId
|
|
145
|
+
) {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return pickCursorFields(cursor);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Builds the next manifest.sync value after a sync operation for the resolved
|
|
154
|
+
* binding (D-237). Named targets write into manifest.sync.targets.<name>
|
|
155
|
+
* (stamped with their api_url/project_id identity); when the binding is the
|
|
156
|
+
* default target, the flat fields mirror the same cursor so ≤0.7.0 CLIs stay
|
|
157
|
+
* correct on the default environment. Legacy flat bindings keep writing the
|
|
158
|
+
* flat fields only.
|
|
159
|
+
*/
|
|
160
|
+
export function buildSyncStateWithCursor(manifestSync, binding, cursor) {
|
|
161
|
+
const existing = manifestSync && typeof manifestSync === 'object' ? manifestSync : {};
|
|
162
|
+
const cursorFields = pickCursorFields(cursor);
|
|
163
|
+
|
|
164
|
+
if (!binding?.target) {
|
|
165
|
+
return { ...existing, ...cursorFields };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const targets = {
|
|
169
|
+
...(existing.targets && typeof existing.targets === 'object' ? existing.targets : {}),
|
|
170
|
+
[binding.target]: {
|
|
171
|
+
api_url: binding.apiUrl,
|
|
172
|
+
project_id: binding.projectId,
|
|
173
|
+
...cursorFields,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...existing,
|
|
179
|
+
...(binding.isDefault ? cursorFields : {}),
|
|
180
|
+
targets,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Migrates a legacy flat manifest cursor into a named target's cursor slot
|
|
186
|
+
* (D-237). Only applies when the new target's binding values match the flat
|
|
187
|
+
* binding the cursor was written against, the target has no cursor yet, and
|
|
188
|
+
* a flat cursor actually exists. Returns the next manifest.sync value, or
|
|
189
|
+
* null when nothing should change.
|
|
190
|
+
*/
|
|
191
|
+
export function migrateFlatCursorToTarget(manifestSync, targetName, bindingFields) {
|
|
192
|
+
const existing = manifestSync && typeof manifestSync === 'object' ? manifestSync : {};
|
|
193
|
+
const targets = existing.targets && typeof existing.targets === 'object' ? existing.targets : {};
|
|
194
|
+
const flatCursor = pickCursorFields(existing);
|
|
195
|
+
|
|
196
|
+
if (targets[targetName] || Object.keys(flatCursor).length === 0) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...existing,
|
|
202
|
+
targets: {
|
|
203
|
+
...targets,
|
|
204
|
+
[targetName]: {
|
|
205
|
+
api_url: bindingFields.apiUrl,
|
|
206
|
+
project_id: bindingFields.projectId,
|
|
207
|
+
...flatCursor,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Reconciles manifest cursor state after connect-hosted (re)binds a named
|
|
215
|
+
* target (D-237). Handles, in order:
|
|
216
|
+
* 1. first flat→named conversion with matching values: migrate the flat
|
|
217
|
+
* cursor into the target's slot;
|
|
218
|
+
* 2. rebinding an existing target to a different api_url/project_id: drop its
|
|
219
|
+
* now-wrong cursor;
|
|
220
|
+
* 3. when the (re)bound target is the default: re-mirror the flat cursor from
|
|
221
|
+
* the target's identity-matching cursor, or clear it — so an older CLI can
|
|
222
|
+
* never combine the new flat binding with a previous environment's cursor.
|
|
223
|
+
* Returns the next manifest.sync value, or null when nothing changes.
|
|
224
|
+
*/
|
|
225
|
+
export function reconcileCursorsAfterConnect(manifestSync, targetName, bindingFields, options = {}) {
|
|
226
|
+
const existing = manifestSync && typeof manifestSync === 'object' ? manifestSync : {};
|
|
227
|
+
let next = existing;
|
|
228
|
+
let changed = false;
|
|
229
|
+
|
|
230
|
+
if (options.isFirstConversion && options.flatBindingMatches) {
|
|
231
|
+
const migrated = migrateFlatCursorToTarget(next, targetName, bindingFields);
|
|
232
|
+
if (migrated) {
|
|
233
|
+
next = migrated;
|
|
234
|
+
changed = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const targetCursor = next.targets?.[targetName];
|
|
239
|
+
if (
|
|
240
|
+
targetCursor &&
|
|
241
|
+
typeof targetCursor === 'object' &&
|
|
242
|
+
(targetCursor.api_url !== bindingFields.apiUrl ||
|
|
243
|
+
targetCursor.project_id !== bindingFields.projectId)
|
|
244
|
+
) {
|
|
245
|
+
const targets = { ...next.targets };
|
|
246
|
+
delete targets[targetName];
|
|
247
|
+
next = { ...next, targets };
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (options.isDefault) {
|
|
252
|
+
const mirrored = mirrorTargetCursorToFlat(next, targetName, bindingFields);
|
|
253
|
+
if (JSON.stringify(mirrored) !== JSON.stringify(next)) {
|
|
254
|
+
next = mirrored;
|
|
255
|
+
changed = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return changed ? next : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Re-mirrors the flat manifest cursor from a (new default) target's cursor
|
|
264
|
+
* (D-237). When the target has no identity-matching cursor, the flat cursor
|
|
265
|
+
* fields are cleared so an older CLI cannot reuse the previous default's
|
|
266
|
+
* cursor against the new default environment.
|
|
267
|
+
*/
|
|
268
|
+
export function mirrorTargetCursorToFlat(manifestSync, targetName, bindingFields) {
|
|
269
|
+
const existing = manifestSync && typeof manifestSync === 'object' ? manifestSync : {};
|
|
270
|
+
const base = {};
|
|
271
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
272
|
+
if (!CURSOR_FIELDS.includes(key)) {
|
|
273
|
+
base[key] = value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const target = existing.targets?.[targetName];
|
|
278
|
+
const identityMatches =
|
|
279
|
+
target &&
|
|
280
|
+
typeof target === 'object' &&
|
|
281
|
+
target.api_url === bindingFields.apiUrl &&
|
|
282
|
+
target.project_id === bindingFields.projectId;
|
|
283
|
+
|
|
284
|
+
return identityMatches ? { ...base, ...pickCursorFields(target) } : base;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Builds the project.yaml sync section for a set of named targets. The flat
|
|
289
|
+
* fields always mirror the default target so older CLIs that only read the
|
|
290
|
+
* flat binding keep working against the default environment.
|
|
291
|
+
*/
|
|
292
|
+
export function buildSyncSectionWithTargets(targets, defaultTarget) {
|
|
293
|
+
const defaultFields = normalizeBindingFields(targets[defaultTarget]);
|
|
294
|
+
if (!defaultFields) {
|
|
295
|
+
throw new Error(`Default sync target "${defaultTarget}" is incomplete.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
provider: 'vibecompass',
|
|
300
|
+
api_url: defaultFields.apiUrl,
|
|
301
|
+
project_id: defaultFields.projectId,
|
|
302
|
+
credential_source: 'env',
|
|
303
|
+
credential_env_var: defaultFields.credentialEnvVar,
|
|
304
|
+
default_target: defaultTarget,
|
|
305
|
+
targets,
|
|
306
|
+
};
|
|
307
|
+
}
|
package/src/sync.js
CHANGED
|
@@ -3,6 +3,11 @@ import path from 'node:path';
|
|
|
3
3
|
import { sha256Text } from './hash.js';
|
|
4
4
|
import { writeStateManifest } from './manifest.js';
|
|
5
5
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
6
|
+
import {
|
|
7
|
+
buildSyncStateWithCursor,
|
|
8
|
+
readSyncCursor,
|
|
9
|
+
resolveSyncBinding,
|
|
10
|
+
} from './sync-binding.js';
|
|
6
11
|
|
|
7
12
|
const DEFAULT_EXPORT_PATH = 'state/pull-export.json';
|
|
8
13
|
|
|
@@ -14,7 +19,8 @@ export async function pushProjectMemory(options = {}, environment = {}) {
|
|
|
14
19
|
});
|
|
15
20
|
const manifest = manifestResult.manifest;
|
|
16
21
|
const documents = await readPushDocuments(context.rootDir, manifest.documents);
|
|
17
|
-
const
|
|
22
|
+
const cursor = readSyncCursor(manifest.sync, context.binding);
|
|
23
|
+
const baseRemoteRevisionId = normalizeUuid(cursor.last_successful_remote_revision);
|
|
18
24
|
const body = {
|
|
19
25
|
...(baseRemoteRevisionId ? { base_remote_revision_id: baseRemoteRevisionId } : {}),
|
|
20
26
|
local_root_revision: manifest.canonical.local_root_revision,
|
|
@@ -25,15 +31,15 @@ export async function pushProjectMemory(options = {}, environment = {}) {
|
|
|
25
31
|
|
|
26
32
|
const response = await postJson(context, 'push', body);
|
|
27
33
|
const now = new Date().toISOString();
|
|
28
|
-
const sync = {
|
|
29
|
-
...
|
|
34
|
+
const sync = buildSyncStateWithCursor(manifest.sync, context.binding, {
|
|
35
|
+
...cursor,
|
|
30
36
|
last_successful_remote_revision: response.remote_revision_id,
|
|
31
37
|
last_successful_local_root_revision: manifest.canonical.local_root_revision,
|
|
32
38
|
last_successful_manifest_hash: manifest.canonical.manifest_hash,
|
|
33
39
|
last_sync_direction: 'push',
|
|
34
40
|
last_sync_at: now,
|
|
35
41
|
pending_previews: [],
|
|
36
|
-
};
|
|
42
|
+
});
|
|
37
43
|
const updatedManifest = await writeStateManifest(context.rootDir, { sync });
|
|
38
44
|
|
|
39
45
|
return {
|
|
@@ -55,10 +61,11 @@ export async function pullPreviewProjectMemory(options = {}, environment = {}) {
|
|
|
55
61
|
sync: existingManifest?.sync,
|
|
56
62
|
});
|
|
57
63
|
const manifest = manifestResult.manifest;
|
|
64
|
+
const cursor = readSyncCursor(manifest.sync, context.binding);
|
|
58
65
|
|
|
59
66
|
const response = await postJson(context, 'pull-preview', {
|
|
60
67
|
state_version: manifest.state_version,
|
|
61
|
-
base_remote_revision_id: normalizeUuid(
|
|
68
|
+
base_remote_revision_id: normalizeUuid(cursor.last_successful_remote_revision),
|
|
62
69
|
local_root_revision: manifest.canonical.local_root_revision,
|
|
63
70
|
local_manifest_hash: manifest.canonical.manifest_hash,
|
|
64
71
|
local_documents: buildLocalDocumentSummaries(manifest.documents),
|
|
@@ -81,13 +88,13 @@ export async function pullPreviewProjectMemory(options = {}, environment = {}) {
|
|
|
81
88
|
created_at: response.created_at ?? new Date().toISOString(),
|
|
82
89
|
expires_at: response.expires_at,
|
|
83
90
|
};
|
|
84
|
-
const sync = {
|
|
85
|
-
...
|
|
91
|
+
const sync = buildSyncStateWithCursor(manifest.sync, context.binding, {
|
|
92
|
+
...cursor,
|
|
86
93
|
pending_previews: [
|
|
87
|
-
...(Array.isArray(
|
|
94
|
+
...(Array.isArray(cursor.pending_previews) ? cursor.pending_previews : []),
|
|
88
95
|
previewRecord,
|
|
89
96
|
].slice(-5),
|
|
90
|
-
};
|
|
97
|
+
});
|
|
91
98
|
const updatedManifest = await writeStateManifest(context.rootDir, { sync });
|
|
92
99
|
|
|
93
100
|
return {
|
|
@@ -105,7 +112,7 @@ export async function pullPreviewProjectMemory(options = {}, environment = {}) {
|
|
|
105
112
|
export async function pullExportProjectMemory(options = {}, environment = {}) {
|
|
106
113
|
const context = await loadSyncContext(options, environment);
|
|
107
114
|
const manifest = await readRequiredManifest(context.manifestPath);
|
|
108
|
-
const preview = resolvePreview(manifest, options.previewToken);
|
|
115
|
+
const preview = resolvePreview(manifest, options.previewToken, context.binding);
|
|
109
116
|
const proposalIds = options.proposalIds?.length
|
|
110
117
|
? options.proposalIds
|
|
111
118
|
: preview.available_proposal_ids ?? [];
|
|
@@ -200,18 +207,15 @@ async function loadSyncContext(options, environment) {
|
|
|
200
207
|
if (project.mode !== 'local-primary') {
|
|
201
208
|
throw new Error('Hosted sync commands require project.yaml mode: local-primary.');
|
|
202
209
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
typeof project.sync.credential_env_var !== 'string'
|
|
208
|
-
) {
|
|
209
|
-
throw new Error('Hosted sync commands require sync.api_url, sync.project_id, and sync.credential_env_var in project.yaml.');
|
|
210
|
+
|
|
211
|
+
const binding = resolveSyncBinding(project, options.syncTarget ?? null);
|
|
212
|
+
if (!binding) {
|
|
213
|
+
throw new Error('Hosted sync commands require sync.api_url, sync.project_id, and sync.credential_env_var in project.yaml (or a named target under sync.targets).');
|
|
210
214
|
}
|
|
211
215
|
|
|
212
|
-
const credential = normalizeOptionalString((environment.env ?? process.env)[
|
|
216
|
+
const credential = normalizeOptionalString((environment.env ?? process.env)[binding.credentialEnvVar]);
|
|
213
217
|
if (!credential) {
|
|
214
|
-
throw new Error(`Hosted sync command requires ${
|
|
218
|
+
throw new Error(`Hosted sync command requires ${binding.credentialEnvVar}.`);
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
const fetchImpl = environment.runtime?.fetch ?? globalThis.fetch;
|
|
@@ -225,8 +229,10 @@ async function loadSyncContext(options, environment) {
|
|
|
225
229
|
manifestPath,
|
|
226
230
|
fetch: fetchImpl,
|
|
227
231
|
credential,
|
|
228
|
-
apiUrl:
|
|
229
|
-
projectId:
|
|
232
|
+
apiUrl: binding.apiUrl,
|
|
233
|
+
projectId: binding.projectId,
|
|
234
|
+
syncTarget: binding.target,
|
|
235
|
+
binding,
|
|
230
236
|
};
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -308,9 +314,10 @@ function buildLocalDocumentExtractedSummary(kind, extracted = {}) {
|
|
|
308
314
|
return {};
|
|
309
315
|
}
|
|
310
316
|
|
|
311
|
-
function resolvePreview(manifest, previewToken) {
|
|
312
|
-
const
|
|
313
|
-
|
|
317
|
+
function resolvePreview(manifest, previewToken, binding) {
|
|
318
|
+
const cursor = readSyncCursor(manifest.sync, binding);
|
|
319
|
+
const previews = Array.isArray(cursor.pending_previews)
|
|
320
|
+
? cursor.pending_previews
|
|
314
321
|
: [];
|
|
315
322
|
if (previewToken) {
|
|
316
323
|
const match = previews.find((preview) => preview.preview_token === previewToken);
|
|
@@ -99,7 +99,7 @@ When the user says \`docs review\`, run this workflow inside the current Claude
|
|
|
99
99
|
|
|
100
100
|
## Goal
|
|
101
101
|
|
|
102
|
-
Turn starter project memory into evidence-backed architecture docs under \`architecture/<domain>/<feature>/<component>.md\`, a projected coverage plan, and optional accepted decision recommendations.
|
|
102
|
+
Turn starter project memory into evidence-backed architecture docs under \`architecture/<domain>/<feature>/<component>.md\`, a projected coverage plan, a journey-aware project map, and optional accepted decision recommendations.
|
|
103
103
|
|
|
104
104
|
## Steps
|
|
105
105
|
|
|
@@ -109,8 +109,8 @@ Turn starter project memory into evidence-backed architecture docs under \`archi
|
|
|
109
109
|
- produce a coverage plan before reading broad source bodies
|
|
110
110
|
- read targeted source files only when the coverage plan needs evidence
|
|
111
111
|
3. Run \`vibecompass docs-review --guided --llm <tool-name> --model <model-name>\` to record the review marker and print the canonical docs-review contract. Use the current AI tool name and model when known.
|
|
112
|
-
4. Present the evidence inventory, coverage plan, proposed architecture docs, blindspots, token-budget risks, and any decision candidates before generating accepted output.
|
|
113
|
-
5. Draft only output the user accepts. The accepted plan must be fenced as \`vibecompass-coverage-plan version=1\`. Accepted architecture docs must be fenced as \`vibecompass-architecture-doc path=architecture/domain/feature/component.md\` blocks and must include required frontmatter: \`domain\`, \`feature\`, \`component\`, and \`status\`. Accepted local decisions may be fenced as \`vibecompass-decision-recommendation target=decisions/domain.md\` blocks without D-numbers.
|
|
112
|
+
4. Present the evidence inventory, coverage plan, user journey, project/system map, proposed architecture docs, blindspots, token-budget risks, and any decision candidates before generating accepted output.
|
|
113
|
+
5. Draft only output the user accepts. The accepted plan must be fenced as \`vibecompass-coverage-plan version=1\`. Accepted architecture docs must be fenced as \`vibecompass-architecture-doc path=architecture/domain/feature/component.md\` blocks and must include required frontmatter: \`domain\`, \`feature\`, \`component\`, and \`status\`. The accepted \`architecture/overview/project-shape.md\` doc should include exactly one \`vibecompass-project-map version=1\` JSON block with journey-facing features, entry points, and evidence-backed relationships. Accepted local decisions may be fenced as \`vibecompass-decision-recommendation target=decisions/domain.md\` blocks without D-numbers.
|
|
114
114
|
6. Keep each generated architecture doc focused and retrieval-oriented. Soft budget: under 12000 bytes per doc unless the extra detail is necessary and called out in review metadata or retrieval guidance.
|
|
115
115
|
7. Write the accepted fenced blocks to \`${outputPath}\`.
|
|
116
116
|
8. Run \`vibecompass docs-review --apply-output\` to validate and apply the accepted blocks. If accepted docs were written manually outside the output file, run \`vibecompass docs-review --complete\` after confirming the marker should be completed.
|