@eltonssouza/development-utility-kit 1.0.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.
Files changed (137) hide show
  1. package/.claude/agents/analyst.md +198 -0
  2. package/.claude/agents/backend-developer.md +126 -0
  3. package/.claude/agents/brain-keeper.md +229 -0
  4. package/.claude/agents/code-reviewer.md +181 -0
  5. package/.claude/agents/database-engineer.md +94 -0
  6. package/.claude/agents/devops-engineer.md +141 -0
  7. package/.claude/agents/frontend-developer.md +97 -0
  8. package/.claude/agents/gate-keeper.md +118 -0
  9. package/.claude/agents/migrator.md +291 -0
  10. package/.claude/agents/mobile-developer.md +80 -0
  11. package/.claude/agents/n8n-specialist.md +94 -0
  12. package/.claude/agents/product-owner.md +115 -0
  13. package/.claude/agents/qa-engineer.md +232 -0
  14. package/.claude/agents/release-engineer.md +204 -0
  15. package/.claude/agents/scaffold.md +87 -0
  16. package/.claude/agents/security-engineer.md +199 -0
  17. package/.claude/agents/sprint-runner.md +44 -0
  18. package/.claude/agents/stack-resolver.md +84 -0
  19. package/.claude/agents/tech-lead.md +182 -0
  20. package/.claude/agents/update-template.md +54 -0
  21. package/.claude/agents/ux-designer.md +118 -0
  22. package/.claude/settings.json +44 -0
  23. package/.claude/skills/README.md +332 -0
  24. package/.claude/skills/active-project/SKILL.md +129 -0
  25. package/.claude/skills/api-integration-test/SKILL.md +64 -0
  26. package/.claude/skills/auto-test-guard/SKILL.md +237 -0
  27. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  28. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  29. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  30. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  31. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  32. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  33. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  34. package/.claude/skills/brain-keeper/SKILL.md +60 -0
  35. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  36. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  37. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  38. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  39. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  40. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  41. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  42. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  43. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  44. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  45. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  46. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  47. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  48. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  49. package/.claude/skills/caveman/SKILL.md +187 -0
  50. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  51. package/.claude/skills/grill-me/SKILL.md +79 -0
  52. package/.claude/skills/honcho-memory/SKILL.md +207 -0
  53. package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
  54. package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
  55. package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
  56. package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
  57. package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
  58. package/.claude/skills/honcho-memory/package.json +32 -0
  59. package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
  60. package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
  61. package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
  62. package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
  63. package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
  64. package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
  65. package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
  66. package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
  67. package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
  68. package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
  69. package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
  70. package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
  71. package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
  72. package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
  73. package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
  74. package/.claude/skills/pair-debug/SKILL.md +288 -0
  75. package/.claude/skills/prd-ready-check/SKILL.md +58 -0
  76. package/.claude/skills/project-manager/SKILL.md +167 -0
  77. package/.claude/skills/quality-standards/SKILL.md +201 -0
  78. package/.claude/skills/quick-feature/SKILL.md +264 -0
  79. package/.claude/skills/run-sprint/SKILL.md +342 -0
  80. package/.claude/skills/scaffold/SKILL.md +58 -0
  81. package/.claude/skills/stack-discovery/SKILL.md +159 -0
  82. package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
  83. package/.claude/skills/to-issues/SKILL.md +163 -0
  84. package/.claude/skills/to-prd/SKILL.md +130 -0
  85. package/.claude/skills/update-template/SKILL.md +254 -0
  86. package/.claude/stacks/CODEOWNERS +30 -0
  87. package/.claude/stacks/README.md +88 -0
  88. package/.claude/stacks/_template.md +116 -0
  89. package/.claude/stacks/java/spring-boot-3.md +376 -0
  90. package/.claude/stacks/java/spring-boot-4.md +438 -0
  91. package/.claude/stacks/typescript/angular-18.md +420 -0
  92. package/.claude/stacks/typescript/angular-19.md +397 -0
  93. package/.claude/stacks/typescript/angular-21.md +494 -0
  94. package/CLAUDE.md +453 -0
  95. package/README.md +391 -0
  96. package/bin/cli.js +773 -0
  97. package/bin/lib/backup.js +62 -0
  98. package/bin/lib/detect-stack.js +476 -0
  99. package/bin/lib/help.js +233 -0
  100. package/bin/lib/identity.js +108 -0
  101. package/bin/lib/local-dir.js +69 -0
  102. package/bin/lib/manifest.js +236 -0
  103. package/bin/lib/sync-all.js +394 -0
  104. package/bin/lib/version-check.js +398 -0
  105. package/dashboard/db.js +199 -0
  106. package/dashboard/package.json +22 -0
  107. package/dashboard/public/app.js +709 -0
  108. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  109. package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
  110. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  111. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  112. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  113. package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
  114. package/dashboard/public/content/docs/pipeline.en.md +400 -0
  115. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  116. package/dashboard/public/content/docs/skills-reference.en.md +500 -0
  117. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  118. package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
  119. package/dashboard/public/content/manifest.json +102 -0
  120. package/dashboard/public/content/manual/backend.en.md +1138 -0
  121. package/dashboard/public/content/manual/existing-project.en.md +831 -0
  122. package/dashboard/public/content/manual/frontend.en.md +1065 -0
  123. package/dashboard/public/content/manual/fullstack.en.md +1508 -0
  124. package/dashboard/public/content/manual/mobile.en.md +866 -0
  125. package/dashboard/public/index.html +108 -0
  126. package/dashboard/public/style.css +610 -0
  127. package/dashboard/public/vendor/marked.min.js +69 -0
  128. package/dashboard/rtk.js +143 -0
  129. package/dashboard/server-app.js +403 -0
  130. package/dashboard/server.js +104 -0
  131. package/dashboard/test/sprint1.test.js +406 -0
  132. package/dashboard/test/sprint2.test.js +571 -0
  133. package/dashboard/test/sprint3.test.js +560 -0
  134. package/package.json +33 -0
  135. package/scripts/hooks/subagent-telemetry.sh +14 -0
  136. package/scripts/hooks/telemetry-writer.js +250 -0
  137. package/scripts/latest-versions.json +56 -0
@@ -0,0 +1,394 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sync-all.js — Batch update of harness across multiple project subdirectories.
5
+ *
6
+ * Default = dry-run preview. `--apply` actually invokes the adoption pipeline
7
+ * via the main CLI's adoptProject() function.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { compareVersions } = require('./version-check');
13
+
14
+ const HARNESS_DIR_SKIP = new Set([
15
+ 'node_modules',
16
+ '.git',
17
+ '.idea',
18
+ '.vscode',
19
+ 'dist',
20
+ 'build',
21
+ 'target',
22
+ 'out',
23
+ ]);
24
+
25
+ /**
26
+ * @typedef {{
27
+ * key: 'stack'|'type'|'age'|'harness-version',
28
+ * value: string|number,
29
+ * op: string
30
+ * }} ParsedFilter
31
+ */
32
+
33
+ /**
34
+ * @typedef {{
35
+ * name: string,
36
+ * path: string,
37
+ * harnessVersion: string|null,
38
+ * projectType: string|null,
39
+ * primaryStack: string|null,
40
+ * ageMs: number
41
+ * }} ProjectCandidate
42
+ */
43
+
44
+ // ─── filter parsing ──────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Convert a duration string ("30d", "6m", "1y", "12h") to milliseconds.
48
+ * @param {string} s
49
+ * @returns {number|null}
50
+ */
51
+ function parseDuration(s) {
52
+ if (!s || typeof s !== 'string') return null;
53
+ const m = s.match(/^(\d+)([hdmy])$/i);
54
+ if (!m) return null;
55
+ const n = parseInt(m[1], 10);
56
+ const unit = m[2].toLowerCase();
57
+ switch (unit) {
58
+ case 'h':
59
+ return n * 60 * 60 * 1000;
60
+ case 'd':
61
+ return n * 24 * 60 * 60 * 1000;
62
+ case 'm':
63
+ return n * 30 * 24 * 60 * 60 * 1000; // month ≈ 30d
64
+ case 'y':
65
+ return n * 365 * 24 * 60 * 60 * 1000;
66
+ default:
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Parse a single filter expression like "stack:java" or "harness-version:<0.2".
73
+ * @param {string} expr
74
+ * @returns {ParsedFilter|null}
75
+ */
76
+ function parseFilterExpr(expr) {
77
+ if (!expr || typeof expr !== 'string') return null;
78
+ const idx = expr.indexOf(':');
79
+ if (idx === -1) return null;
80
+ const key = expr.slice(0, idx).trim().toLowerCase();
81
+ const rawValue = expr.slice(idx + 1).trim();
82
+ if (!rawValue) return null;
83
+
84
+ switch (key) {
85
+ case 'stack':
86
+ case 'type':
87
+ return { key, value: rawValue.toLowerCase(), op: '=' };
88
+ case 'age': {
89
+ const ms = parseDuration(rawValue);
90
+ if (ms === null) return null;
91
+ return { key, value: ms, op: '>=' }; // ageMs >= threshold = "older than"
92
+ }
93
+ case 'harness-version': {
94
+ const opMatch = rawValue.match(/^(>=|<=|>|<|=)/);
95
+ if (opMatch) {
96
+ return { key, value: rawValue, op: opMatch[1] };
97
+ }
98
+ return { key, value: '=' + rawValue, op: '=' };
99
+ }
100
+ default:
101
+ return null;
102
+ }
103
+ }
104
+
105
+ // ─── CLAUDE.md parsing ───────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Extract a key from the `## Project Identity` block of a CLAUDE.md string.
109
+ * Returns the raw value (without surrounding backticks).
110
+ * @param {string} content
111
+ * @param {string} key
112
+ * @returns {string|null}
113
+ */
114
+ function extractIdentityField(content, key) {
115
+ if (!content) return null;
116
+ const idIdx = content.indexOf('## Project Identity');
117
+ if (idIdx === -1) return null;
118
+ const nextIdx = content.indexOf('\n## ', idIdx + 1);
119
+ const block = nextIdx === -1 ? content.slice(idIdx) : content.slice(idIdx, nextIdx);
120
+
121
+ const re = new RegExp(
122
+ `[*-]\\s*\\*\\*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\*\\*\\s*:\\s*\`?([^\`\n]+?)\`?\\s*$`,
123
+ 'im'
124
+ );
125
+ const m = block.match(re);
126
+ return m ? m[1].trim() : null;
127
+ }
128
+
129
+ // ─── project discovery ──────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Find candidate projects under dir (subdirs containing .claude/).
133
+ * @param {string} dir
134
+ * @returns {ProjectCandidate[]}
135
+ */
136
+ function discoverProjects(dir) {
137
+ const out = [];
138
+ let entries;
139
+ try {
140
+ entries = fs.readdirSync(dir, { withFileTypes: true });
141
+ } catch {
142
+ return out;
143
+ }
144
+
145
+ for (const ent of entries) {
146
+ if (!ent.isDirectory()) continue;
147
+ if (ent.name.startsWith('.')) continue;
148
+ if (HARNESS_DIR_SKIP.has(ent.name)) continue;
149
+
150
+ const projectPath = path.join(dir, ent.name);
151
+ const claudeDir = path.join(projectPath, '.claude');
152
+ if (!fs.existsSync(claudeDir)) continue;
153
+
154
+ const candidate = inspectProject(projectPath);
155
+ if (candidate) out.push(candidate);
156
+ }
157
+
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * Build a ProjectCandidate by reading .claude/HARNESS_VERSION + CLAUDE.md.
163
+ * @param {string} projectPath
164
+ * @returns {ProjectCandidate|null}
165
+ */
166
+ function inspectProject(projectPath) {
167
+ const claudeDir = path.join(projectPath, '.claude');
168
+ const versionFile = path.join(claudeDir, 'HARNESS_VERSION');
169
+ const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
170
+
171
+ let harnessVersion = null;
172
+ try {
173
+ harnessVersion = fs.readFileSync(versionFile, 'utf8').trim() || null;
174
+ } catch {
175
+ harnessVersion = null;
176
+ }
177
+
178
+ let projectType = null;
179
+ let primaryStack = null;
180
+ try {
181
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
182
+ projectType = extractIdentityField(content, 'Project type');
183
+ primaryStack = extractIdentityField(content, 'Primary stack');
184
+ } catch {
185
+ // no CLAUDE.md or unreadable
186
+ }
187
+
188
+ let ageMs = 0;
189
+ try {
190
+ const stat = fs.statSync(claudeDir);
191
+ ageMs = Date.now() - stat.mtimeMs;
192
+ } catch {
193
+ ageMs = 0;
194
+ }
195
+
196
+ return {
197
+ name: path.basename(projectPath),
198
+ path: projectPath,
199
+ harnessVersion,
200
+ projectType,
201
+ primaryStack,
202
+ ageMs,
203
+ };
204
+ }
205
+
206
+ // ─── filter application ──────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Check whether a project matches one parsed filter.
210
+ * @param {ProjectCandidate} proj
211
+ * @param {ParsedFilter} filter
212
+ * @returns {boolean}
213
+ */
214
+ function matchesFilter(proj, filter) {
215
+ if (!filter) return true;
216
+ switch (filter.key) {
217
+ case 'stack': {
218
+ const stack = (proj.primaryStack || '').toLowerCase();
219
+ return stack.includes(String(filter.value));
220
+ }
221
+ case 'type': {
222
+ const t = (proj.projectType || '').toLowerCase();
223
+ return t === String(filter.value) || t.includes(String(filter.value));
224
+ }
225
+ case 'age': {
226
+ return proj.ageMs >= Number(filter.value);
227
+ }
228
+ case 'harness-version': {
229
+ if (!proj.harnessVersion) return false;
230
+ const expr = String(filter.value);
231
+ const cmp = compareVersions(proj.harnessVersion, expr);
232
+ // compareVersions returns -1 when the version satisfies the constraint
233
+ // (or 0 for "=X" matches)
234
+ return cmp <= 0;
235
+ }
236
+ default:
237
+ return true;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Apply all filters (AND) and excludes (NOT) to a candidate list.
243
+ * @param {ProjectCandidate[]} candidates
244
+ * @param {ParsedFilter[]} filters
245
+ * @param {string[]} excludes
246
+ * @returns {ProjectCandidate[]}
247
+ */
248
+ function applyFilters(candidates, filters, excludes) {
249
+ const excludeSet = new Set((excludes || []).map((x) => x.toLowerCase()));
250
+ return candidates.filter((c) => {
251
+ if (excludeSet.has(c.name.toLowerCase())) return false;
252
+ return filters.every((f) => matchesFilter(c, f));
253
+ });
254
+ }
255
+
256
+ // ─── humanizers ──────────────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Convert ageMs to a short human-readable string.
260
+ * @param {number} ms
261
+ * @returns {string}
262
+ */
263
+ function humanAge(ms) {
264
+ if (!ms || ms < 0) return 'unknown';
265
+ const days = Math.floor(ms / (24 * 60 * 60 * 1000));
266
+ if (days < 1) return '<1d';
267
+ if (days < 30) return `${days}d`;
268
+ const months = Math.floor(days / 30);
269
+ if (months < 12) return `${months}m`;
270
+ const years = Math.floor(days / 365);
271
+ return `${years}y`;
272
+ }
273
+
274
+ // ─── runSyncAll ──────────────────────────────────────────────────────────────
275
+
276
+ /**
277
+ * @typedef {{ filters: string[], excludes: string[], apply: boolean,
278
+ * adopt?: (opts: {cwd: string, sub: string|null, dryRun: boolean}) => void }} SyncOptions
279
+ */
280
+
281
+ /**
282
+ * Run sync-all over a directory.
283
+ *
284
+ * The `adopt` option lets the CLI inject its own adoptProject implementation
285
+ * (avoids a circular require with bin/cli.js). When omitted, lazy-loads
286
+ * `../cli.js#adoptProject`.
287
+ *
288
+ * @param {string} dir
289
+ * @param {SyncOptions} options
290
+ * @returns {{ matched: ProjectCandidate[], updated: number, skipped: number, errors: number }}
291
+ */
292
+ function runSyncAll(dir, options) {
293
+ const opts = options || {};
294
+ const filterStrs = opts.filters || [];
295
+ const excludes = opts.excludes || [];
296
+ const apply = !!opts.apply;
297
+
298
+ if (!dir || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
299
+ process.stderr.write(`Error: directory "${dir}" not found or not a directory.\n`);
300
+ process.exit(1);
301
+ }
302
+
303
+ // Parse filters
304
+ const parsedFilters = [];
305
+ for (const f of filterStrs) {
306
+ const parsed = parseFilterExpr(f);
307
+ if (!parsed) {
308
+ process.stderr.write(`Warning: ignoring unrecognized filter "${f}"\n`);
309
+ continue;
310
+ }
311
+ parsedFilters.push(parsed);
312
+ }
313
+
314
+ // Discover + filter
315
+ const candidates = discoverProjects(dir);
316
+ const matched = applyFilters(candidates, parsedFilters, excludes);
317
+
318
+ // Print plan
319
+ process.stdout.write(`\nkud sync-all — scanning ${dir}\n`);
320
+ process.stdout.write(`Found ${candidates.length} project(s) with .claude/\n`);
321
+ if (parsedFilters.length) {
322
+ process.stdout.write(`Filters: ${filterStrs.join(', ')}\n`);
323
+ }
324
+ if (excludes.length) {
325
+ process.stdout.write(`Excludes: ${excludes.join(', ')}\n`);
326
+ }
327
+ process.stdout.write(`Matched ${matched.length} project(s)${apply ? '' : ' (dry-run)'}\n\n`);
328
+
329
+ if (matched.length === 0) {
330
+ process.stdout.write('Nothing to do.\n');
331
+ return { matched: [], updated: 0, skipped: 0, errors: 0 };
332
+ }
333
+
334
+ // Print table
335
+ for (const p of matched) {
336
+ process.stdout.write(
337
+ ` ${p.name.padEnd(30)} ` +
338
+ `v${(p.harnessVersion || '?').padEnd(8)} ` +
339
+ `${(p.projectType || '?').padEnd(15)} ` +
340
+ `age:${humanAge(p.ageMs).padEnd(6)} ` +
341
+ `${(p.primaryStack || '').slice(0, 40)}\n`
342
+ );
343
+ }
344
+
345
+ if (!apply) {
346
+ process.stdout.write('\nDry-run only. Re-run with --apply to perform updates.\n');
347
+ return { matched, updated: 0, skipped: 0, errors: 0 };
348
+ }
349
+
350
+ // Resolve adopt function (lazy require to avoid cycles)
351
+ let adopt = opts.adopt;
352
+ if (!adopt) {
353
+ try {
354
+ const cli = require('../cli');
355
+ adopt = cli.adoptProject;
356
+ } catch (e) {
357
+ process.stderr.write(`Error: could not load adoptProject from bin/cli.js — ${e.message}\n`);
358
+ return { matched, updated: 0, skipped: 0, errors: matched.length };
359
+ }
360
+ }
361
+
362
+ // Apply per project
363
+ let updated = 0;
364
+ let errors = 0;
365
+ process.stdout.write('\nApplying updates...\n');
366
+ for (const p of matched) {
367
+ try {
368
+ adopt({ cwd: p.path, sub: null, dryRun: false });
369
+ process.stdout.write(` [OK] ${p.name}\n`);
370
+ updated += 1;
371
+ } catch (e) {
372
+ process.stdout.write(` [FAIL] ${p.name} — ${e.message}\n`);
373
+ errors += 1;
374
+ }
375
+ }
376
+
377
+ process.stdout.write(
378
+ `\nSummary: updated=${updated}, errors=${errors}, skipped=${matched.length - updated - errors}\n`
379
+ );
380
+ return { matched, updated, skipped: matched.length - updated - errors, errors };
381
+ }
382
+
383
+ module.exports = {
384
+ runSyncAll,
385
+ // exported for tests
386
+ parseDuration,
387
+ parseFilterExpr,
388
+ extractIdentityField,
389
+ discoverProjects,
390
+ inspectProject,
391
+ matchesFilter,
392
+ applyFilters,
393
+ humanAge,
394
+ };