@karmaniverous/jeeves-meta 0.11.0 → 0.11.2

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,32 @@ 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 using character-class wrapping.
849
+ *
850
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
851
+ * the path separator. Instead, each metacharacter is wrapped in a character
852
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
853
+ *
854
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
855
+ *
856
+ * @param s - Raw path string.
857
+ * @returns String with glob metacharacters wrapped in character classes.
858
+ */
859
+ function escapeGlob(s) {
860
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
861
+ }
862
+
837
863
  /**
838
864
  * Filter file paths by modification time.
839
865
  *
@@ -934,7 +960,7 @@ function filterInScope(node, files) {
934
960
  */
935
961
  async function getScopeFiles(node, watcher, logger) {
936
962
  const walkStart = Date.now();
937
- const rawFiles = await watcher.walk([`${node.ownerPath}/**`]);
963
+ const rawFiles = await watcher.walk([`${escapeGlob(node.ownerPath)}/**`]);
938
964
  const allFiles = rawFiles.map(normalizePath);
939
965
  const scopeFiles = filterInScope(node, allFiles);
940
966
  logger?.debug({
@@ -1746,7 +1772,7 @@ async function buildMinimalNode(metaPath, watcher) {
1746
1772
  // We include only *direct* children (nearest descendants in the ownership tree)
1747
1773
  // to match the ownership semantics used elsewhere.
1748
1774
  const rawMetaJsonPaths = await watcher.walk([
1749
- `${ownerPath}/**/.meta/meta.json`,
1775
+ `${escapeGlob(ownerPath)}/**/.meta/meta.json`,
1750
1776
  ]);
1751
1777
  const candidateMetaPaths = [
1752
1778
  ...new Set(rawMetaJsonPaths.map((p) => normalizePath(dirname(p)))),
@@ -1882,7 +1908,7 @@ function discoverStalestPath(candidates, depthWeight) {
1882
1908
  async function isStale(scopePrefix, meta, watcher) {
1883
1909
  if (!meta._generatedAt)
1884
1910
  return true; // Never synthesized = stale
1885
- const files = await watcher.walk([`${scopePrefix}/**`]);
1911
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
1886
1912
  return hasModifiedAfter(files, new Date(meta._generatedAt).getTime());
1887
1913
  }
1888
1914
  /** Maximum staleness for never-synthesized metas (1 year in seconds). */
@@ -2159,6 +2185,14 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
2159
2185
  Object.keys(ctx.childMetas).length > 0 ||
2160
2186
  Object.keys(ctx.crossRefMetas).length > 0;
2161
2187
  if (!hasScope && !currentMeta._content) {
2188
+ // Bump _generatedAt so this entity doesn't keep winning the staleness
2189
+ // race every cycle. It will be re-evaluated when files appear.
2190
+ // Uses lock-staging for atomic write consistency.
2191
+ currentMeta._generatedAt = new Date().toISOString();
2192
+ const lockPath = join(node.metaPath, '.lock');
2193
+ const metaJsonPath = join(node.metaPath, 'meta.json');
2194
+ await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
2195
+ await copyFile(lockPath, metaJsonPath);
2162
2196
  logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
2163
2197
  return { synthesized: false };
2164
2198
  }
@@ -11111,6 +11145,11 @@ async function startService(config, configPath) {
11111
11145
  // orchestrate() always returns exactly one result
11112
11146
  const result = results[0];
11113
11147
  const durationMs = Date.now() - startMs;
11148
+ if (!result.synthesized) {
11149
+ // Entity was skipped (e.g. empty scope) — no progress to report.
11150
+ logger.debug({ path: ownerPath }, 'Synthesis skipped');
11151
+ return;
11152
+ }
11114
11153
  // Update stats
11115
11154
  stats.totalSyntheses++;
11116
11155
  stats.lastCycleDurationMs = durationMs;
package/dist/index.js CHANGED
@@ -826,6 +826,32 @@ 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 using character-class wrapping.
841
+ *
842
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
843
+ * the path separator. Instead, each metacharacter is wrapped in a character
844
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
845
+ *
846
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
847
+ *
848
+ * @param s - Raw path string.
849
+ * @returns String with glob metacharacters wrapped in character classes.
850
+ */
851
+ function escapeGlob(s) {
852
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
853
+ }
854
+
829
855
  /**
830
856
  * Filter file paths by modification time.
831
857
  *
@@ -926,7 +952,7 @@ function filterInScope(node, files) {
926
952
  */
927
953
  async function getScopeFiles(node, watcher, logger) {
928
954
  const walkStart = Date.now();
929
- const rawFiles = await watcher.walk([`${node.ownerPath}/**`]);
955
+ const rawFiles = await watcher.walk([`${escapeGlob(node.ownerPath)}/**`]);
930
956
  const allFiles = rawFiles.map(normalizePath);
931
957
  const scopeFiles = filterInScope(node, allFiles);
932
958
  logger?.debug({
@@ -1738,7 +1764,7 @@ async function buildMinimalNode(metaPath, watcher) {
1738
1764
  // We include only *direct* children (nearest descendants in the ownership tree)
1739
1765
  // to match the ownership semantics used elsewhere.
1740
1766
  const rawMetaJsonPaths = await watcher.walk([
1741
- `${ownerPath}/**/.meta/meta.json`,
1767
+ `${escapeGlob(ownerPath)}/**/.meta/meta.json`,
1742
1768
  ]);
1743
1769
  const candidateMetaPaths = [
1744
1770
  ...new Set(rawMetaJsonPaths.map((p) => normalizePath(dirname(p)))),
@@ -1874,7 +1900,7 @@ function discoverStalestPath(candidates, depthWeight) {
1874
1900
  async function isStale(scopePrefix, meta, watcher) {
1875
1901
  if (!meta._generatedAt)
1876
1902
  return true; // Never synthesized = stale
1877
- const files = await watcher.walk([`${scopePrefix}/**`]);
1903
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
1878
1904
  return hasModifiedAfter(files, new Date(meta._generatedAt).getTime());
1879
1905
  }
1880
1906
  /** Maximum staleness for never-synthesized metas (1 year in seconds). */
@@ -2151,6 +2177,14 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
2151
2177
  Object.keys(ctx.childMetas).length > 0 ||
2152
2178
  Object.keys(ctx.crossRefMetas).length > 0;
2153
2179
  if (!hasScope && !currentMeta._content) {
2180
+ // Bump _generatedAt so this entity doesn't keep winning the staleness
2181
+ // race every cycle. It will be re-evaluated when files appear.
2182
+ // Uses lock-staging for atomic write consistency.
2183
+ currentMeta._generatedAt = new Date().toISOString();
2184
+ const lockPath = join(node.metaPath, '.lock');
2185
+ const metaJsonPath = join(node.metaPath, 'meta.json');
2186
+ await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
2187
+ await copyFile(lockPath, metaJsonPath);
2154
2188
  logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
2155
2189
  return { synthesized: false };
2156
2190
  }
@@ -11103,6 +11137,11 @@ async function startService(config, configPath) {
11103
11137
  // orchestrate() always returns exactly one result
11104
11138
  const result = results[0];
11105
11139
  const durationMs = Date.now() - startMs;
11140
+ if (!result.synthesized) {
11141
+ // Entity was skipped (e.g. empty scope) — no progress to report.
11142
+ logger.debug({ path: ownerPath }, 'Synthesis skipped');
11143
+ return;
11144
+ }
11106
11145
  // Update stats
11107
11146
  stats.totalSyntheses++;
11108
11147
  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.2",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Fastify HTTP service for the Jeeves Meta synthesis engine",
6
6
  "license": "BSD-3-Clause",