@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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-meta",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Fastify HTTP service for the Jeeves Meta synthesis engine",
6
6
  "license": "BSD-3-Clause",