@karmaniverous/jeeves-meta 0.10.1 → 0.11.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/dist/cli/jeeves-meta/index.js +82 -15
- package/dist/index.js +82 -15
- package/package.json +1 -1
|
@@ -46,11 +46,11 @@ const metaConfigSchema = z.object({
|
|
|
46
46
|
/** Maximum lines of context to include in subprocess prompts. */
|
|
47
47
|
maxLines: z.number().int().min(50).default(500),
|
|
48
48
|
/** Architect subprocess timeout in seconds. */
|
|
49
|
-
architectTimeout: z.number().int().min(30).default(
|
|
49
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
50
50
|
/** Builder subprocess timeout in seconds. */
|
|
51
|
-
builderTimeout: z.number().int().min(60).default(
|
|
51
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
52
52
|
/** Critic subprocess timeout in seconds. */
|
|
53
|
-
criticTimeout: z.number().int().min(30).default(
|
|
53
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
54
54
|
/** Thinking level for spawned synthesis sessions. */
|
|
55
55
|
thinking: z.string().default('low'),
|
|
56
56
|
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
@@ -834,6 +834,26 @@ async function listMetas(config, watcher) {
|
|
|
834
834
|
};
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Escape special glob characters in a path so it can be used as a literal
|
|
839
|
+
* prefix in glob patterns.
|
|
840
|
+
*
|
|
841
|
+
* Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
|
|
842
|
+
* that paths containing parentheses (e.g. Slack channel IDs) or other
|
|
843
|
+
* special characters are matched literally by the watcher's walk endpoint.
|
|
844
|
+
*
|
|
845
|
+
* @module escapeGlob
|
|
846
|
+
*/
|
|
847
|
+
/**
|
|
848
|
+
* Escape glob metacharacters in a string.
|
|
849
|
+
*
|
|
850
|
+
* @param s - Raw path string.
|
|
851
|
+
* @returns String with glob metacharacters backslash-escaped.
|
|
852
|
+
*/
|
|
853
|
+
function escapeGlob(s) {
|
|
854
|
+
return s.replace(/[*?[\]{}()!]/g, '\\$&');
|
|
855
|
+
}
|
|
856
|
+
|
|
837
857
|
/**
|
|
838
858
|
* Filter file paths by modification time.
|
|
839
859
|
*
|
|
@@ -934,7 +954,7 @@ function filterInScope(node, files) {
|
|
|
934
954
|
*/
|
|
935
955
|
async function getScopeFiles(node, watcher, logger) {
|
|
936
956
|
const walkStart = Date.now();
|
|
937
|
-
const rawFiles = await watcher.walk([`${node.ownerPath}/**`]);
|
|
957
|
+
const rawFiles = await watcher.walk([`${escapeGlob(node.ownerPath)}/**`]);
|
|
938
958
|
const allFiles = rawFiles.map(normalizePath);
|
|
939
959
|
const scopeFiles = filterInScope(node, allFiles);
|
|
940
960
|
logger?.debug({
|
|
@@ -1746,7 +1766,7 @@ async function buildMinimalNode(metaPath, watcher) {
|
|
|
1746
1766
|
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1747
1767
|
// to match the ownership semantics used elsewhere.
|
|
1748
1768
|
const rawMetaJsonPaths = await watcher.walk([
|
|
1749
|
-
`${ownerPath}/**/.meta/meta.json`,
|
|
1769
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1750
1770
|
]);
|
|
1751
1771
|
const candidateMetaPaths = [
|
|
1752
1772
|
...new Set(rawMetaJsonPaths.map((p) => normalizePath(dirname(p)))),
|
|
@@ -1882,7 +1902,7 @@ function discoverStalestPath(candidates, depthWeight) {
|
|
|
1882
1902
|
async function isStale(scopePrefix, meta, watcher) {
|
|
1883
1903
|
if (!meta._generatedAt)
|
|
1884
1904
|
return true; // Never synthesized = stale
|
|
1885
|
-
const files = await watcher.walk([`${scopePrefix}/**`]);
|
|
1905
|
+
const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
|
|
1886
1906
|
return hasModifiedAfter(files, new Date(meta._generatedAt).getTime());
|
|
1887
1907
|
}
|
|
1888
1908
|
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
@@ -2151,6 +2171,25 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2151
2171
|
const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
2152
2172
|
// Step 7: Compute context (includes scope files and delta files)
|
|
2153
2173
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
2174
|
+
// Skip empty-scope entities that have no prior content.
|
|
2175
|
+
// Without scope files, child metas, or cross-refs there is nothing for
|
|
2176
|
+
// the architect/builder to work with and the cycle will either time out
|
|
2177
|
+
// or produce empty output.
|
|
2178
|
+
const hasScope = ctx.scopeFiles.length > 0 ||
|
|
2179
|
+
Object.keys(ctx.childMetas).length > 0 ||
|
|
2180
|
+
Object.keys(ctx.crossRefMetas).length > 0;
|
|
2181
|
+
if (!hasScope && !currentMeta._content) {
|
|
2182
|
+
// Bump _generatedAt so this entity doesn't keep winning the staleness
|
|
2183
|
+
// race every cycle. It will be re-evaluated when files appear.
|
|
2184
|
+
// Uses lock-staging for atomic write consistency.
|
|
2185
|
+
currentMeta._generatedAt = new Date().toISOString();
|
|
2186
|
+
const lockPath = join(node.metaPath, '.lock');
|
|
2187
|
+
const metaJsonPath = join(node.metaPath, 'meta.json');
|
|
2188
|
+
await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
|
|
2189
|
+
await copyFile(lockPath, metaJsonPath);
|
|
2190
|
+
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
2191
|
+
return { synthesized: false };
|
|
2192
|
+
}
|
|
2154
2193
|
// Step 5 (deferred): Structure hash from context scope files
|
|
2155
2194
|
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
2156
2195
|
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
@@ -2447,21 +2486,41 @@ function formatSeconds(durationMs) {
|
|
|
2447
2486
|
function titleCasePhase(phase) {
|
|
2448
2487
|
return phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
2449
2488
|
}
|
|
2450
|
-
/**
|
|
2451
|
-
|
|
2452
|
-
|
|
2489
|
+
/**
|
|
2490
|
+
* URL-encode each path segment individually so that spaces and special
|
|
2491
|
+
* characters are safe while preserving the `/` separators.
|
|
2492
|
+
*/
|
|
2493
|
+
function encodePathSegments(p) {
|
|
2494
|
+
return p
|
|
2495
|
+
.split('/')
|
|
2496
|
+
.map((seg) => encodeURIComponent(seg))
|
|
2497
|
+
.join('/');
|
|
2498
|
+
}
|
|
2499
|
+
/** Build a link (or plain path) to the owner directory. */
|
|
2500
|
+
function buildDirectoryLink(path, serverBaseUrl) {
|
|
2501
|
+
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2502
|
+
const encoded = encodePathSegments(normalized);
|
|
2503
|
+
if (!serverBaseUrl)
|
|
2504
|
+
return normalized;
|
|
2505
|
+
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2506
|
+
return `${base}/path${encoded}`;
|
|
2507
|
+
}
|
|
2508
|
+
/** Build a link (or plain path) to the entity's meta.json output file. */
|
|
2509
|
+
function buildMetaJsonLink(path, serverBaseUrl) {
|
|
2453
2510
|
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2454
2511
|
const metaJsonPath = `${normalized}/.meta/meta.json`;
|
|
2512
|
+
const encoded = encodePathSegments(metaJsonPath);
|
|
2455
2513
|
if (!serverBaseUrl)
|
|
2456
2514
|
return metaJsonPath;
|
|
2457
2515
|
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2458
|
-
return `${base}/path${
|
|
2516
|
+
return `${base}/path${encoded}`;
|
|
2459
2517
|
}
|
|
2460
2518
|
function formatProgressEvent(event, serverBaseUrl) {
|
|
2461
|
-
const pathDisplay = buildEntityLink(event.path, serverBaseUrl);
|
|
2462
2519
|
switch (event.type) {
|
|
2463
|
-
case 'synthesis_start':
|
|
2464
|
-
|
|
2520
|
+
case 'synthesis_start': {
|
|
2521
|
+
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
2522
|
+
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
2523
|
+
}
|
|
2465
2524
|
case 'phase_start': {
|
|
2466
2525
|
if (!event.phase) {
|
|
2467
2526
|
return ' ⚙️ Phase started';
|
|
@@ -2475,16 +2534,18 @@ function formatProgressEvent(event, serverBaseUrl) {
|
|
|
2475
2534
|
return ` ✅ ${phase} complete (${formatNumber(tokens)} tokens / ${duration})`;
|
|
2476
2535
|
}
|
|
2477
2536
|
case 'synthesis_complete': {
|
|
2537
|
+
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
|
|
2478
2538
|
const tokens = event.tokens ?? 0;
|
|
2479
2539
|
const duration = event.durationMs !== undefined
|
|
2480
2540
|
? formatSeconds(event.durationMs)
|
|
2481
2541
|
: '0.0s';
|
|
2482
|
-
return `✅ Completed: ${
|
|
2542
|
+
return `✅ Completed: ${metaLink} (${formatNumber(tokens)} tokens / ${duration})`;
|
|
2483
2543
|
}
|
|
2484
2544
|
case 'error': {
|
|
2545
|
+
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
2485
2546
|
const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
|
|
2486
2547
|
const error = event.error ?? 'Unknown error';
|
|
2487
|
-
return `❌ Synthesis failed at ${phase}phase: ${
|
|
2548
|
+
return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
|
|
2488
2549
|
}
|
|
2489
2550
|
default: {
|
|
2490
2551
|
return 'Unknown progress event';
|
|
@@ -11078,6 +11139,11 @@ async function startService(config, configPath) {
|
|
|
11078
11139
|
// orchestrate() always returns exactly one result
|
|
11079
11140
|
const result = results[0];
|
|
11080
11141
|
const durationMs = Date.now() - startMs;
|
|
11142
|
+
if (!result.synthesized) {
|
|
11143
|
+
// Entity was skipped (e.g. empty scope) — no progress to report.
|
|
11144
|
+
logger.debug({ path: ownerPath }, 'Synthesis skipped');
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
11081
11147
|
// Update stats
|
|
11082
11148
|
stats.totalSyntheses++;
|
|
11083
11149
|
stats.lastCycleDurationMs = durationMs;
|
|
@@ -11087,6 +11153,7 @@ async function startService(config, configPath) {
|
|
|
11087
11153
|
await progress.report({
|
|
11088
11154
|
type: 'error',
|
|
11089
11155
|
path: ownerPath,
|
|
11156
|
+
phase: result.error.step,
|
|
11090
11157
|
error: result.error.message,
|
|
11091
11158
|
});
|
|
11092
11159
|
}
|
package/dist/index.js
CHANGED
|
@@ -254,11 +254,11 @@ const metaConfigSchema = z.object({
|
|
|
254
254
|
/** Maximum lines of context to include in subprocess prompts. */
|
|
255
255
|
maxLines: z.number().int().min(50).default(500),
|
|
256
256
|
/** Architect subprocess timeout in seconds. */
|
|
257
|
-
architectTimeout: z.number().int().min(30).default(
|
|
257
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
258
258
|
/** Builder subprocess timeout in seconds. */
|
|
259
|
-
builderTimeout: z.number().int().min(60).default(
|
|
259
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
260
260
|
/** Critic subprocess timeout in seconds. */
|
|
261
|
-
criticTimeout: z.number().int().min(30).default(
|
|
261
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
262
262
|
/** Thinking level for spawned synthesis sessions. */
|
|
263
263
|
thinking: z.string().default('low'),
|
|
264
264
|
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
@@ -826,6 +826,26 @@ async function listMetas(config, watcher) {
|
|
|
826
826
|
};
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
+
/**
|
|
830
|
+
* Escape special glob characters in a path so it can be used as a literal
|
|
831
|
+
* prefix in glob patterns.
|
|
832
|
+
*
|
|
833
|
+
* Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
|
|
834
|
+
* that paths containing parentheses (e.g. Slack channel IDs) or other
|
|
835
|
+
* special characters are matched literally by the watcher's walk endpoint.
|
|
836
|
+
*
|
|
837
|
+
* @module escapeGlob
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
840
|
+
* Escape glob metacharacters in a string.
|
|
841
|
+
*
|
|
842
|
+
* @param s - Raw path string.
|
|
843
|
+
* @returns String with glob metacharacters backslash-escaped.
|
|
844
|
+
*/
|
|
845
|
+
function escapeGlob(s) {
|
|
846
|
+
return s.replace(/[*?[\]{}()!]/g, '\\$&');
|
|
847
|
+
}
|
|
848
|
+
|
|
829
849
|
/**
|
|
830
850
|
* Filter file paths by modification time.
|
|
831
851
|
*
|
|
@@ -926,7 +946,7 @@ function filterInScope(node, files) {
|
|
|
926
946
|
*/
|
|
927
947
|
async function getScopeFiles(node, watcher, logger) {
|
|
928
948
|
const walkStart = Date.now();
|
|
929
|
-
const rawFiles = await watcher.walk([`${node.ownerPath}/**`]);
|
|
949
|
+
const rawFiles = await watcher.walk([`${escapeGlob(node.ownerPath)}/**`]);
|
|
930
950
|
const allFiles = rawFiles.map(normalizePath);
|
|
931
951
|
const scopeFiles = filterInScope(node, allFiles);
|
|
932
952
|
logger?.debug({
|
|
@@ -1738,7 +1758,7 @@ async function buildMinimalNode(metaPath, watcher) {
|
|
|
1738
1758
|
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1739
1759
|
// to match the ownership semantics used elsewhere.
|
|
1740
1760
|
const rawMetaJsonPaths = await watcher.walk([
|
|
1741
|
-
`${ownerPath}/**/.meta/meta.json`,
|
|
1761
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1742
1762
|
]);
|
|
1743
1763
|
const candidateMetaPaths = [
|
|
1744
1764
|
...new Set(rawMetaJsonPaths.map((p) => normalizePath(dirname(p)))),
|
|
@@ -1874,7 +1894,7 @@ function discoverStalestPath(candidates, depthWeight) {
|
|
|
1874
1894
|
async function isStale(scopePrefix, meta, watcher) {
|
|
1875
1895
|
if (!meta._generatedAt)
|
|
1876
1896
|
return true; // Never synthesized = stale
|
|
1877
|
-
const files = await watcher.walk([`${scopePrefix}/**`]);
|
|
1897
|
+
const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
|
|
1878
1898
|
return hasModifiedAfter(files, new Date(meta._generatedAt).getTime());
|
|
1879
1899
|
}
|
|
1880
1900
|
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
@@ -2143,6 +2163,25 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2143
2163
|
const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
2144
2164
|
// Step 7: Compute context (includes scope files and delta files)
|
|
2145
2165
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
2166
|
+
// Skip empty-scope entities that have no prior content.
|
|
2167
|
+
// Without scope files, child metas, or cross-refs there is nothing for
|
|
2168
|
+
// the architect/builder to work with and the cycle will either time out
|
|
2169
|
+
// or produce empty output.
|
|
2170
|
+
const hasScope = ctx.scopeFiles.length > 0 ||
|
|
2171
|
+
Object.keys(ctx.childMetas).length > 0 ||
|
|
2172
|
+
Object.keys(ctx.crossRefMetas).length > 0;
|
|
2173
|
+
if (!hasScope && !currentMeta._content) {
|
|
2174
|
+
// Bump _generatedAt so this entity doesn't keep winning the staleness
|
|
2175
|
+
// race every cycle. It will be re-evaluated when files appear.
|
|
2176
|
+
// Uses lock-staging for atomic write consistency.
|
|
2177
|
+
currentMeta._generatedAt = new Date().toISOString();
|
|
2178
|
+
const lockPath = join(node.metaPath, '.lock');
|
|
2179
|
+
const metaJsonPath = join(node.metaPath, 'meta.json');
|
|
2180
|
+
await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
|
|
2181
|
+
await copyFile(lockPath, metaJsonPath);
|
|
2182
|
+
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
2183
|
+
return { synthesized: false };
|
|
2184
|
+
}
|
|
2146
2185
|
// Step 5 (deferred): Structure hash from context scope files
|
|
2147
2186
|
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
2148
2187
|
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
@@ -2439,21 +2478,41 @@ function formatSeconds(durationMs) {
|
|
|
2439
2478
|
function titleCasePhase(phase) {
|
|
2440
2479
|
return phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
2441
2480
|
}
|
|
2442
|
-
/**
|
|
2443
|
-
|
|
2444
|
-
|
|
2481
|
+
/**
|
|
2482
|
+
* URL-encode each path segment individually so that spaces and special
|
|
2483
|
+
* characters are safe while preserving the `/` separators.
|
|
2484
|
+
*/
|
|
2485
|
+
function encodePathSegments(p) {
|
|
2486
|
+
return p
|
|
2487
|
+
.split('/')
|
|
2488
|
+
.map((seg) => encodeURIComponent(seg))
|
|
2489
|
+
.join('/');
|
|
2490
|
+
}
|
|
2491
|
+
/** Build a link (or plain path) to the owner directory. */
|
|
2492
|
+
function buildDirectoryLink(path, serverBaseUrl) {
|
|
2493
|
+
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2494
|
+
const encoded = encodePathSegments(normalized);
|
|
2495
|
+
if (!serverBaseUrl)
|
|
2496
|
+
return normalized;
|
|
2497
|
+
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2498
|
+
return `${base}/path${encoded}`;
|
|
2499
|
+
}
|
|
2500
|
+
/** Build a link (or plain path) to the entity's meta.json output file. */
|
|
2501
|
+
function buildMetaJsonLink(path, serverBaseUrl) {
|
|
2445
2502
|
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2446
2503
|
const metaJsonPath = `${normalized}/.meta/meta.json`;
|
|
2504
|
+
const encoded = encodePathSegments(metaJsonPath);
|
|
2447
2505
|
if (!serverBaseUrl)
|
|
2448
2506
|
return metaJsonPath;
|
|
2449
2507
|
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2450
|
-
return `${base}/path${
|
|
2508
|
+
return `${base}/path${encoded}`;
|
|
2451
2509
|
}
|
|
2452
2510
|
function formatProgressEvent(event, serverBaseUrl) {
|
|
2453
|
-
const pathDisplay = buildEntityLink(event.path, serverBaseUrl);
|
|
2454
2511
|
switch (event.type) {
|
|
2455
|
-
case 'synthesis_start':
|
|
2456
|
-
|
|
2512
|
+
case 'synthesis_start': {
|
|
2513
|
+
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
2514
|
+
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
2515
|
+
}
|
|
2457
2516
|
case 'phase_start': {
|
|
2458
2517
|
if (!event.phase) {
|
|
2459
2518
|
return ' ⚙️ Phase started';
|
|
@@ -2467,16 +2526,18 @@ function formatProgressEvent(event, serverBaseUrl) {
|
|
|
2467
2526
|
return ` ✅ ${phase} complete (${formatNumber(tokens)} tokens / ${duration})`;
|
|
2468
2527
|
}
|
|
2469
2528
|
case 'synthesis_complete': {
|
|
2529
|
+
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
|
|
2470
2530
|
const tokens = event.tokens ?? 0;
|
|
2471
2531
|
const duration = event.durationMs !== undefined
|
|
2472
2532
|
? formatSeconds(event.durationMs)
|
|
2473
2533
|
: '0.0s';
|
|
2474
|
-
return `✅ Completed: ${
|
|
2534
|
+
return `✅ Completed: ${metaLink} (${formatNumber(tokens)} tokens / ${duration})`;
|
|
2475
2535
|
}
|
|
2476
2536
|
case 'error': {
|
|
2537
|
+
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
2477
2538
|
const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
|
|
2478
2539
|
const error = event.error ?? 'Unknown error';
|
|
2479
|
-
return `❌ Synthesis failed at ${phase}phase: ${
|
|
2540
|
+
return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
|
|
2480
2541
|
}
|
|
2481
2542
|
default: {
|
|
2482
2543
|
return 'Unknown progress event';
|
|
@@ -11070,6 +11131,11 @@ async function startService(config, configPath) {
|
|
|
11070
11131
|
// orchestrate() always returns exactly one result
|
|
11071
11132
|
const result = results[0];
|
|
11072
11133
|
const durationMs = Date.now() - startMs;
|
|
11134
|
+
if (!result.synthesized) {
|
|
11135
|
+
// Entity was skipped (e.g. empty scope) — no progress to report.
|
|
11136
|
+
logger.debug({ path: ownerPath }, 'Synthesis skipped');
|
|
11137
|
+
return;
|
|
11138
|
+
}
|
|
11073
11139
|
// Update stats
|
|
11074
11140
|
stats.totalSyntheses++;
|
|
11075
11141
|
stats.lastCycleDurationMs = durationMs;
|
|
@@ -11079,6 +11145,7 @@ async function startService(config, configPath) {
|
|
|
11079
11145
|
await progress.report({
|
|
11080
11146
|
type: 'error',
|
|
11081
11147
|
path: ownerPath,
|
|
11148
|
+
phase: result.error.step,
|
|
11082
11149
|
error: result.error.message,
|
|
11083
11150
|
});
|
|
11084
11151
|
}
|