@karmaniverous/jeeves-meta 0.11.0 → 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 +36 -3
- package/dist/index.js +36 -3
- package/package.json +1 -1
|
@@ -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). */
|
|
@@ -2159,6 +2179,14 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2159
2179
|
Object.keys(ctx.childMetas).length > 0 ||
|
|
2160
2180
|
Object.keys(ctx.crossRefMetas).length > 0;
|
|
2161
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);
|
|
2162
2190
|
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
2163
2191
|
return { synthesized: false };
|
|
2164
2192
|
}
|
|
@@ -11111,6 +11139,11 @@ async function startService(config, configPath) {
|
|
|
11111
11139
|
// orchestrate() always returns exactly one result
|
|
11112
11140
|
const result = results[0];
|
|
11113
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
|
+
}
|
|
11114
11147
|
// Update stats
|
|
11115
11148
|
stats.totalSyntheses++;
|
|
11116
11149
|
stats.lastCycleDurationMs = durationMs;
|
package/dist/index.js
CHANGED
|
@@ -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). */
|
|
@@ -2151,6 +2171,14 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2151
2171
|
Object.keys(ctx.childMetas).length > 0 ||
|
|
2152
2172
|
Object.keys(ctx.crossRefMetas).length > 0;
|
|
2153
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);
|
|
2154
2182
|
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
2155
2183
|
return { synthesized: false };
|
|
2156
2184
|
}
|
|
@@ -11103,6 +11131,11 @@ async function startService(config, configPath) {
|
|
|
11103
11131
|
// orchestrate() always returns exactly one result
|
|
11104
11132
|
const result = results[0];
|
|
11105
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
|
+
}
|
|
11106
11139
|
// Update stats
|
|
11107
11140
|
stats.totalSyntheses++;
|
|
11108
11141
|
stats.lastCycleDurationMs = durationMs;
|