@wonsukchoi/crondex 0.17.0 → 0.19.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.
- package/README.md +43 -40
- package/catalog.json +979 -33
- package/jobs/agriculture/grain-storage-temperature-check.yaml +34 -0
- package/jobs/automotive/warranty-claim-deadline-check.yaml +35 -0
- package/jobs/childcare/allergy-action-plan-expiry-check.yaml +34 -0
- package/jobs/construction/material-delivery-delay-check.yaml +32 -0
- package/jobs/creator/brand-deal-payment-followup-check.yaml +32 -0
- package/jobs/crypto/stablecoin-peg-deviation-watch.yaml +33 -0
- package/jobs/ecommerce/shipping-delay-check.yaml +31 -0
- package/jobs/education/overdue-library-materials-check.yaml +34 -0
- package/jobs/events/speaker-confirmation-deadline-check.yaml +35 -0
- package/jobs/fitness/locker-rental-expiry-check.yaml +35 -0
- package/jobs/fleet/fleet-insurance-expiry-check.yaml +34 -0
- package/jobs/gaming/game-server-disk-space-check.yaml +34 -0
- package/jobs/government/public-comment-period-closing-check.yaml +35 -0
- package/jobs/growth/seat-utilization-upsell-check.yaml +34 -0
- package/jobs/healthcare/medical-equipment-calibration-check.yaml +35 -0
- package/jobs/hiring/job-posting-expiry-check.yaml +41 -0
- package/jobs/home/utility-bill-anomaly-check.yaml +36 -0
- package/jobs/hospitality/housekeeping-turnover-time-check.yaml +41 -0
- package/jobs/hr/i9-reverification-deadline-check.yaml +36 -0
- package/jobs/insurance/beneficiary-designation-review-reminder.yaml +35 -0
- package/jobs/inventory/supplier-lead-time-drift-check.yaml +34 -0
- package/jobs/investing/bond-cd-maturity-reminder.yaml +31 -0
- package/jobs/learning/practice-streak-check.yaml +33 -0
- package/jobs/legal/business-license-renewal-check.yaml +37 -0
- package/jobs/logistics/port-congestion-watch.yaml +34 -0
- package/jobs/manufacturing/equipment-calibration-due-check.yaml +32 -0
- package/jobs/marketing/newsletter-unsubscribe-spike-check.yaml +37 -0
- package/jobs/nonprofit/event-rsvp-shortfall-check.yaml +40 -0
- package/jobs/podcast/transcript-publish-lag-check.yaml +31 -0
- package/jobs/realestate/hoa-fee-collection-check.yaml +35 -0
- package/jobs/restaurant/supplier-price-increase-watch.yaml +36 -0
- package/jobs/retail/markdown-aging-inventory-check.yaml +33 -0
- package/jobs/sales/proposal-followup-nudge.yaml +31 -0
- package/jobs/support/macro-usage-staleness-check.yaml +33 -0
- package/jobs/team/expense-report-deadline-reminder.yaml +28 -0
- package/jobs/travel/hotel-cancellation-deadline-reminder.yaml +32 -0
- package/jobs/veterinary/medication-inventory-expiry-check.yaml +31 -0
- package/jobs/warehousing/returns-processing-backlog-check.yaml +33 -0
- package/lib/recommend.js +75 -2
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
id: medication-inventory-expiry-check
|
|
2
|
+
version: 1
|
|
3
|
+
name: Medication Inventory Expiry Check
|
|
4
|
+
description: >
|
|
5
|
+
Scans the clinic's drug and vaccine inventory log for items expiring
|
|
6
|
+
soon so they can be used, returned, or discarded before they go bad.
|
|
7
|
+
Use this if expired medication has ever turned up still on the shelf
|
|
8
|
+
during an audit.
|
|
9
|
+
category: veterinary
|
|
10
|
+
tags: [veterinary, inventory, medication, expiry]
|
|
11
|
+
schedule: "0 8 * * 1"
|
|
12
|
+
timezone: "UTC"
|
|
13
|
+
runner: shell
|
|
14
|
+
command: >
|
|
15
|
+
today_epoch=$(date +%s); warn_days={{warn_days_before_expiry}};
|
|
16
|
+
{ read -r header; while IFS=',' read -r drug lot expiry; do
|
|
17
|
+
[ -z "$drug" ] && continue;
|
|
18
|
+
exp_epoch=$(date -j -f %Y-%m-%d "$expiry" +%s 2>/dev/null || date -d "$expiry" +%s);
|
|
19
|
+
days_left=$(( (exp_epoch-today_epoch)/86400 ));
|
|
20
|
+
if [ "$days_left" -lt 0 ]; then echo "EXPIRED: $drug (lot $lot) expired on $expiry — remove from shelf.";
|
|
21
|
+
elif [ "$days_left" -le "$warn_days" ]; then echo "EXPIRING SOON: $drug (lot $lot), $days_left day(s) left ($expiry)."; fi
|
|
22
|
+
done; } < "{{inventory_csv_path}}"
|
|
23
|
+
variables:
|
|
24
|
+
inventory_csv_path:
|
|
25
|
+
default: "./medication-inventory.csv"
|
|
26
|
+
description: "Path to a CSV with columns: drug_name,lot_number,expiry_date (YYYY-MM-DD)."
|
|
27
|
+
warn_days_before_expiry:
|
|
28
|
+
default: 30
|
|
29
|
+
description: Flag items expiring within this many days.
|
|
30
|
+
compatible_agents: [generic, claude, codex]
|
|
31
|
+
notes: Zero-token. Read-only check — always visually confirm an item before discarding or returning it.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
id: returns-processing-backlog-check
|
|
2
|
+
version: 1
|
|
3
|
+
name: Returns Processing Backlog Check
|
|
4
|
+
description: >
|
|
5
|
+
Scans the returns log for items that have sat unprocessed longer than
|
|
6
|
+
your target turnaround time. Use this if returned inventory has ever
|
|
7
|
+
piled up because nobody was tracking how long items had been waiting
|
|
8
|
+
to be inspected and restocked or disposed of.
|
|
9
|
+
category: warehousing
|
|
10
|
+
tags: [warehousing, returns, backlog, logistics]
|
|
11
|
+
schedule: "0 7 * * *"
|
|
12
|
+
timezone: "UTC"
|
|
13
|
+
runner: shell
|
|
14
|
+
command: >
|
|
15
|
+
today_epoch=$(date +%s); max_days={{max_days_in_queue}};
|
|
16
|
+
{ read -r header; while IFS=',' read -r rma_id received_date status; do
|
|
17
|
+
[ -z "$rma_id" ] && continue;
|
|
18
|
+
[ "$status" = "processed" ] && continue;
|
|
19
|
+
recv_epoch=$(date -j -f %Y-%m-%d "$received_date" +%s 2>/dev/null || date -d "$received_date" +%s);
|
|
20
|
+
days_waiting=$(( (today_epoch-recv_epoch)/86400 ));
|
|
21
|
+
if [ "$days_waiting" -ge "$max_days" ]; then
|
|
22
|
+
echo "BACKLOGGED: RMA $rma_id received $received_date, $days_waiting day(s) unprocessed (status: $status).";
|
|
23
|
+
fi
|
|
24
|
+
done; } < "{{returns_log_csv_path}}"
|
|
25
|
+
variables:
|
|
26
|
+
returns_log_csv_path:
|
|
27
|
+
default: "./returns-log.csv"
|
|
28
|
+
description: "Path to a CSV with columns: rma_id,received_date (YYYY-MM-DD),status (processed/pending/etc)."
|
|
29
|
+
max_days_in_queue:
|
|
30
|
+
default: 3
|
|
31
|
+
description: Flag returns that have sat unprocessed this many days or longer.
|
|
32
|
+
compatible_agents: [generic, claude, codex]
|
|
33
|
+
notes: Zero-token. Assumes one row per return in the CSV; any status other than "processed" is treated as still in queue.
|
package/lib/recommend.js
CHANGED
|
@@ -16,13 +16,81 @@ export function stem(word) {
|
|
|
16
16
|
return word;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Groups of interchangeable words, grounded in the catalog's actual vocabulary (its
|
|
20
|
+
// most common tags/description verbs) rather than a generic thesaurus — this closes
|
|
21
|
+
// the gap where a query uses a reasonable synonym the job authors didn't happen to
|
|
22
|
+
// write (e.g. "notify" vs. a job tagged "reminder"). Every word in a group maps to
|
|
23
|
+
// the group's first entry, applied on both the query and the job's own fields so
|
|
24
|
+
// either side using a different synonym still lines up.
|
|
25
|
+
const SYNONYM_GROUPS = [
|
|
26
|
+
["alert", "warn", "notify", "notification", "remind", "reminder"],
|
|
27
|
+
["check", "monitor", "watch", "scan", "inspect", "verify"],
|
|
28
|
+
["audit", "review", "compliance"],
|
|
29
|
+
["expire", "expiry", "expiration"],
|
|
30
|
+
["backup", "snapshot"],
|
|
31
|
+
["cost", "spend", "spending", "budget", "expense", "billing"],
|
|
32
|
+
["fail", "failure", "error", "crash", "outage"],
|
|
33
|
+
["delete", "remove", "cleanup", "purge", "clear"],
|
|
34
|
+
["update", "upgrade", "refresh", "sync"],
|
|
35
|
+
["duplicate", "dedupe", "dup"],
|
|
36
|
+
["deadline", "due"],
|
|
37
|
+
["schedule", "scheduling", "calendar", "planning"],
|
|
38
|
+
["drift", "diff", "change", "changed"],
|
|
39
|
+
["renew", "renewal"],
|
|
40
|
+
["followup", "follow-up"],
|
|
41
|
+
["invoice", "billing", "payment"],
|
|
42
|
+
["stale", "outdated", "old"],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const SYNONYM_MAP = new Map();
|
|
46
|
+
for (const group of SYNONYM_GROUPS) {
|
|
47
|
+
for (const word of group) SYNONYM_MAP.set(word, group[0]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function canonicalize(word) {
|
|
51
|
+
return SYNONYM_MAP.get(word) ?? word;
|
|
52
|
+
}
|
|
53
|
+
|
|
19
54
|
export function tokenize(text) {
|
|
20
55
|
return text
|
|
21
56
|
.toLowerCase()
|
|
22
57
|
.replace(/[^a-z0-9\s-]/g, " ")
|
|
23
58
|
.split(/[\s-]+/)
|
|
24
59
|
.filter((w) => w.length > 1 && !STOPWORDS.has(w))
|
|
25
|
-
.map(stem)
|
|
60
|
+
.map(stem)
|
|
61
|
+
.map(canonicalize);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Bounded edit distance — used only as a fallback when no exact token match exists,
|
|
65
|
+
// to catch typos/near-misses (e.g. "expiery" vs "expiry") without embeddings or a
|
|
66
|
+
// spellcheck dependency. Short-circuits past `max` so it stays cheap across a whole
|
|
67
|
+
// catalog scan.
|
|
68
|
+
export function editDistance(a, b, max = 2) {
|
|
69
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
70
|
+
const prev = new Array(b.length + 1);
|
|
71
|
+
const curr = new Array(b.length + 1);
|
|
72
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
73
|
+
for (let i = 1; i <= a.length; i++) {
|
|
74
|
+
curr[0] = i;
|
|
75
|
+
let rowMin = curr[0];
|
|
76
|
+
for (let j = 1; j <= b.length; j++) {
|
|
77
|
+
curr[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j - 1], prev[j], curr[j - 1]);
|
|
78
|
+
rowMin = Math.min(rowMin, curr[j]);
|
|
79
|
+
}
|
|
80
|
+
if (rowMin > max) return max + 1;
|
|
81
|
+
for (let j = 0; j <= b.length; j++) prev[j] = curr[j];
|
|
82
|
+
}
|
|
83
|
+
return prev[b.length];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A query token counts as a fuzzy hit against a field token if they're a short edit
|
|
87
|
+
// distance apart — only tried once no exact match exists (see scoreJob), and only for
|
|
88
|
+
// words long enough that a 1-2 character slip is plausibly a typo rather than a
|
|
89
|
+
// genuinely different short word ("ssl" vs "sql" would otherwise false-positive).
|
|
90
|
+
function isFuzzyMatch(queryToken, fieldToken) {
|
|
91
|
+
if (queryToken.length < 5 || fieldToken.length < 5) return false;
|
|
92
|
+
const maxDistance = queryToken.length >= 8 ? 2 : 1;
|
|
93
|
+
return editDistance(queryToken, fieldToken, maxDistance) <= maxDistance;
|
|
26
94
|
}
|
|
27
95
|
|
|
28
96
|
// id and name are near-duplicates of each other (id is just the slugified
|
|
@@ -33,7 +101,7 @@ export const RECOMMEND_WEIGHTS = { tags: 4, title: 3, category: 2, description:
|
|
|
33
101
|
|
|
34
102
|
export function scoreJob(queryTokens, job) {
|
|
35
103
|
const fields = {
|
|
36
|
-
tags: job.tags.map((t) => stem(t.toLowerCase())),
|
|
104
|
+
tags: job.tags.map((t) => canonicalize(stem(t.toLowerCase()))),
|
|
37
105
|
title: [...new Set([...tokenize(job.name), ...tokenize(job.id.replace(/-/g, " "))])],
|
|
38
106
|
category: tokenize(job.category ?? ""),
|
|
39
107
|
description: tokenize(job.description ?? ""),
|
|
@@ -45,6 +113,11 @@ export function scoreJob(queryTokens, job) {
|
|
|
45
113
|
if (fields[field].includes(qt)) {
|
|
46
114
|
score += weight;
|
|
47
115
|
matched.add(qt);
|
|
116
|
+
} else if (fields[field].some((ft) => isFuzzyMatch(qt, ft))) {
|
|
117
|
+
// Fuzzy hits count for half weight (rounded up) — a real match still
|
|
118
|
+
// outranks a typo-distance one for the same field.
|
|
119
|
+
score += Math.ceil(weight / 2);
|
|
120
|
+
matched.add(qt);
|
|
48
121
|
}
|
|
49
122
|
}
|
|
50
123
|
}
|