skilldrop-cli 0.16.6 → 0.16.8
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 +2 -1
- package/catalogue.json +11 -1
- package/model-routing.json +16 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/ai-usage-policy/evals/evals.json +30 -0
- package/packs/ai-engineering/skills/ai-usage-policy/evals/files/2/data-handling-sheet.md +18 -0
- package/packs/ai-engineering/skills/ai-usage-policy/evals/files/2/it-notes.md +31 -0
- package/packs/ai-engineering/skills/ai-usage-policy/evals/files/2/md-email.md +29 -0
- package/packs/api-builder/skills/tool-use-schema-writer/evals/evals.json +29 -0
- package/packs/api-builder/skills/tool-use-schema-writer/evals/files/2/book_appointment.ts +64 -0
- package/packs/career/loops/apply/LOOP.md +97 -0
- package/packs/career/loops/apply/evals/eval_queries.json +9 -0
- package/packs/career/loops/apply/evals/evals.json +27 -0
- package/packs/career/loops/apply/loop.json +52 -0
- package/packs/career/pack.json +77 -0
- package/packs/career/skills/application-form-answers/SKILL.md +146 -0
- package/packs/career/skills/application-form-answers/evals/eval_queries.json +34 -0
- package/packs/career/skills/application-form-answers/evals/evals.json +103 -0
- package/packs/career/skills/application-form-answers/evals/files/1/form.md +17 -0
- package/packs/career/skills/application-form-answers/evals/files/1/posting.md +20 -0
- package/packs/career/skills/application-form-answers/evals/files/1/resume.md +32 -0
- package/packs/career/skills/application-form-answers/evals/files/2/form.md +15 -0
- package/packs/career/skills/application-form-answers/evals/files/2/posting.md +21 -0
- package/packs/career/skills/application-form-answers/evals/files/2/resume.md +32 -0
- package/packs/career/skills/application-form-answers/evals/files/4/form.md +14 -0
- package/packs/career/skills/application-form-answers/evals/files/4/posting.md +25 -0
- package/packs/career/skills/application-form-answers/evals/files/4/resume.md +29 -0
- package/packs/career/skills/application-form-answers/evals/files/5/form.md +15 -0
- package/packs/career/skills/application-form-answers/evals/files/5/posting.md +18 -0
- package/packs/career/skills/application-form-answers/evals/files/5/resume.md +23 -0
- package/packs/career/skills/application-form-answers/evals/files/6/form.md +11 -0
- package/packs/career/skills/application-form-answers/evals/files/6/posting.md +28 -0
- package/packs/career/skills/application-form-answers/evals/files/6/resume.md +35 -0
- package/packs/career/skills/application-form-answers/evals/files/7/form.md +11 -0
- package/packs/career/skills/application-form-answers/evals/files/7/posting.md +19 -0
- package/packs/career/skills/application-form-answers/evals/files/7/resume.md +26 -0
- package/packs/career/skills/application-form-answers/manifest.json +31 -0
- package/packs/career/skills/cover-letter/SKILL.md +132 -0
- package/packs/career/skills/cover-letter/evals/eval_queries.json +34 -0
- package/packs/career/skills/cover-letter/evals/evals.json +100 -0
- package/packs/career/skills/cover-letter/evals/files/1/posting.md +28 -0
- package/packs/career/skills/cover-letter/evals/files/1/resume.md +35 -0
- package/packs/career/skills/cover-letter/evals/files/2/posting.md +20 -0
- package/packs/career/skills/cover-letter/evals/files/2/resume.md +32 -0
- package/packs/career/skills/cover-letter/evals/files/4/posting.md +19 -0
- package/packs/career/skills/cover-letter/evals/files/4/resume.md +26 -0
- package/packs/career/skills/cover-letter/evals/files/5/posting.md +21 -0
- package/packs/career/skills/cover-letter/evals/files/5/resume.md +32 -0
- package/packs/career/skills/cover-letter/evals/files/6/posting.md +25 -0
- package/packs/career/skills/cover-letter/evals/files/6/resume.md +29 -0
- package/packs/career/skills/cover-letter/evals/files/7/posting.md +18 -0
- package/packs/career/skills/cover-letter/evals/files/7/resume.md +23 -0
- package/packs/career/skills/cover-letter/manifest.json +37 -0
- package/packs/career/skills/cover-letter/scripts/claim_check.py +250 -0
- package/packs/career/skills/job-fit-analysis/SKILL.md +152 -0
- package/packs/career/skills/job-fit-analysis/evals/eval_queries.json +34 -0
- package/packs/career/skills/job-fit-analysis/evals/evals.json +102 -0
- package/packs/career/skills/job-fit-analysis/evals/files/1/posting.md +28 -0
- package/packs/career/skills/job-fit-analysis/evals/files/1/resume.md +35 -0
- package/packs/career/skills/job-fit-analysis/evals/files/2/posting.md +20 -0
- package/packs/career/skills/job-fit-analysis/evals/files/2/resume.md +32 -0
- package/packs/career/skills/job-fit-analysis/evals/files/4/posting.md +25 -0
- package/packs/career/skills/job-fit-analysis/evals/files/4/resume.md +29 -0
- package/packs/career/skills/job-fit-analysis/evals/files/5/posting.md +18 -0
- package/packs/career/skills/job-fit-analysis/evals/files/5/resume.md +23 -0
- package/packs/career/skills/job-fit-analysis/evals/files/6/linkedin-about.txt +5 -0
- package/packs/career/skills/job-fit-analysis/evals/files/6/posting.md +25 -0
- package/packs/career/skills/job-fit-analysis/evals/files/6/resume.md +35 -0
- package/packs/career/skills/job-fit-analysis/evals/files/7/resume.md +32 -0
- package/packs/career/skills/job-fit-analysis/manifest.json +31 -0
- package/packs/career/skills/resume-tailor/SKILL.md +144 -0
- package/packs/career/skills/resume-tailor/evals/eval_queries.json +34 -0
- package/packs/career/skills/resume-tailor/evals/evals.json +133 -0
- package/packs/career/skills/resume-tailor/evals/files/1/posting.md +26 -0
- package/packs/career/skills/resume-tailor/evals/files/1/resume.md +32 -0
- package/packs/career/skills/resume-tailor/evals/files/2/posting.md +19 -0
- package/packs/career/skills/resume-tailor/evals/files/2/resume.md +26 -0
- package/packs/career/skills/resume-tailor/evals/files/3/posting.md +21 -0
- package/packs/career/skills/resume-tailor/evals/files/3/resume.md +32 -0
- package/packs/career/skills/resume-tailor/evals/files/4/posting.md +28 -0
- package/packs/career/skills/resume-tailor/evals/files/4/resume.md +35 -0
- package/packs/career/skills/resume-tailor/evals/files/5/posting.md +20 -0
- package/packs/career/skills/resume-tailor/evals/files/5/resume.md +32 -0
- package/packs/career/skills/resume-tailor/evals/files/6/posting.md +25 -0
- package/packs/career/skills/resume-tailor/evals/files/6/resume.md +29 -0
- package/packs/career/skills/resume-tailor/evals/files/7/posting.md +18 -0
- package/packs/career/skills/resume-tailor/evals/files/7/resume.md +23 -0
- package/packs/career/skills/resume-tailor/evals/files/8/posting.md +25 -0
- package/packs/career/skills/resume-tailor/evals/files/8/resume.typ +31 -0
- package/packs/career/skills/resume-tailor/evals/files/9/linkedin-about.txt +5 -0
- package/packs/career/skills/resume-tailor/evals/files/9/posting.md +25 -0
- package/packs/career/skills/resume-tailor/evals/files/9/resume.md +35 -0
- package/packs/career/skills/resume-tailor/manifest.json +37 -0
- package/packs/career/skills/resume-tailor/scripts/claim_check.py +250 -0
- package/packs/data-analytics/skills/metric-definition/evals/evals.json +30 -0
- package/packs/data-analytics/skills/metric-definition/evals/files/2/tables.md +42 -0
- package/packs/data-analytics/skills/metric-definition/evals/files/2/thread.md +40 -0
- package/packs/dev-team/skills/contribution-wizard/evals/evals.json +29 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/AGENTS.md +9 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/CATALOG.md +18 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/CONTRIBUTING.md +45 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/OWNERS.md +7 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/detention-claim-letter/INSTRUCTIONS.md +20 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/detention-claim-letter/skill.yaml +11 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/detention-claim-letter/tests/cases.yaml +25 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/rate-con-extractor/INSTRUCTIONS.md +20 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/rate-con-extractor/skill.yaml +8 -0
- package/packs/dev-team/skills/contribution-wizard/evals/files/2/skills/rate-con-extractor/tests/cases.yaml +32 -0
- package/packs/dev-team/skills/devils-advocate/evals/evals.json +29 -0
- package/packs/dev-team/skills/devils-advocate/evals/files/2/signups.py +116 -0
- package/packs/dev-team/skills/devils-advocate/evals/files/2/test_signups.py +69 -0
- package/packs/dev-team/skills/release-notes/evals/evals.json +29 -0
- package/packs/dev-team/skills/release-notes/evals/files/2/commits.txt +35 -0
- package/packs/research/skills/source-synthesis/evals/evals.json +30 -0
- package/packs/research/skills/source-synthesis/evals/files/2/badge-data.md +27 -0
- package/packs/research/skills/source-synthesis/evals/files/2/director-email.md +16 -0
- package/packs/research/skills/source-synthesis/evals/files/2/exit-interviews.md +17 -0
- package/packs/research/skills/source-synthesis/evals/files/2/practice-weekly-article.md +21 -0
- package/packs/research/skills/source-synthesis/evals/files/2/staff-survey-summary.md +41 -0
- package/packs/solution-architect/skills/db-schema-design/evals/evals.json +29 -0
- package/packs/solution-architect/skills/db-schema-design/evals/files/2/current_tables.sql +29 -0
- package/packs/solution-architect/skills/db-schema-design/evals/files/2/notes.md +53 -0
- package/packs/solution-architect/skills/design-doc/evals/evals.json +29 -0
- package/packs/solution-architect/skills/design-doc/evals/files/2/brief.md +18 -0
- package/packs/solution-architect/skills/design-doc/evals/files/2/eng-notes.md +30 -0
- package/packs/solution-architect/skills/tech-comparison-matrix/evals/evals.json +29 -0
- package/packs/solution-architect/skills/tech-comparison-matrix/evals/files/2/requirements.md +20 -0
- package/packs/solution-architect/skills/tech-comparison-matrix/evals/files/2/vendor-notes.md +29 -0
- package/packs/sre-oncall/skills/postmortem-generator/evals/evals.json +30 -0
- package/packs/sre-oncall/skills/postmortem-generator/evals/files/2/pager-export.txt +22 -0
- package/packs/sre-oncall/skills/postmortem-generator/evals/files/2/slack-thread.txt +28 -0
- package/packs/stakeholder-comms/skills/guide-builder/evals/evals.json +29 -0
- package/packs/stakeholder-comms/skills/guide-builder/evals/files/2/api-notes.md +44 -0
- package/packs/stakeholder-comms/skills/guide-builder/evals/files/2/sample-booking.json +19 -0
- package/profiles.json +2 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List what a tailored resume or cover letter says that the candidate's own material doesn't.
|
|
3
|
+
|
|
4
|
+
python3 claim_check.py DRAFT --source RESUME [--source NOTES ...] [--posting POSTING] [--json]
|
|
5
|
+
|
|
6
|
+
DRAFT is the text you are about to send. Each --source is something the candidate wrote or
|
|
7
|
+
told you: the original resume, notes from the conversation. --posting is the job posting.
|
|
8
|
+
|
|
9
|
+
It reports four kinds of finding, each with the line it is on:
|
|
10
|
+
|
|
11
|
+
number a figure in the draft that no source contains ("35%", "$2M", "team of 8")
|
|
12
|
+
name a named thing in the draft that no source contains: a tool, an employer, a
|
|
13
|
+
certification, a degree ("Kubernetes", "Epic", "Green Belt")
|
|
14
|
+
combination capitalised words that each appear in a source but never together
|
|
15
|
+
("Senior Manager" when the source has "Senior Analyst" and "Office Manager")
|
|
16
|
+
borrowed wording the draft shares with the posting and no source uses. Fine when the
|
|
17
|
+
source shows the same thing in other words; a false claim when it doesn't.
|
|
18
|
+
|
|
19
|
+
A name or number that the posting contains is marked "in posting": right when the draft is
|
|
20
|
+
talking about the employer, wrong when it is claimed as the candidate's own.
|
|
21
|
+
|
|
22
|
+
It reads text, so it cannot tell a true claim from a false one. It tells you where to look.
|
|
23
|
+
Exit status: 0 nothing found, 1 findings, 2 a file could not be read. Standard library only.
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
MULT = {"k": 1e3, "thousand": 1e3, "m": 1e6, "mm": 1e6, "million": 1e6,
|
|
31
|
+
"bn": 1e9, "b": 1e9, "billion": 1e9}
|
|
32
|
+
NUMBER = re.compile(r"(?<![\w.])(\d[\d,]*(?:\.\d+)?)(?:(k|mm|m|bn|b)\b|\s?(thousand|million|billion)\b)?"
|
|
33
|
+
r"(\s?%|\s?percent\b|\s?per cent\b)?", re.I)
|
|
34
|
+
WORD = re.compile(r"(?<![A-Za-z0-9])\.?[A-Za-z][A-Za-z0-9+#']*(?:\.[A-Za-z0-9]+)*")
|
|
35
|
+
LIST_MARK = re.compile(r"^\s*(?:[-*•–>]+|\d+[.)]|#+|\|)\s*")
|
|
36
|
+
LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
|
|
37
|
+
SUFFIXES = ("ations", "ation", "ments", "ment", "ings", "ing", "ers", "ies", "ed", "er", "es", "s", "ly")
|
|
38
|
+
|
|
39
|
+
# Words that start a line or a sentence with a capital letter and name nothing.
|
|
40
|
+
PLAIN = set("""
|
|
41
|
+
a an and are as at be been both but by for from had has have he her his i if in into is it its
|
|
42
|
+
me my no not of on or our she so that the their them then there these they this to up us was
|
|
43
|
+
we were what when where which who why with you your dear sincerely regards kind best yours
|
|
44
|
+
faithfully hiring manager team committee recruiter sir madam re subject date enclosed
|
|
45
|
+
summary profile objective experience employment work history education skills tools
|
|
46
|
+
certifications certification qualifications training projects awards languages interests
|
|
47
|
+
references available request professional career key core technical additional other
|
|
48
|
+
volunteering volunteer contact details achievements highlights about present current
|
|
49
|
+
january february march april may june july august september october november december
|
|
50
|
+
jan feb mar apr jun jul aug sep sept oct nov dec monday tuesday wednesday thursday friday
|
|
51
|
+
saturday sunday full part time application applying while although though because since
|
|
52
|
+
during after before however also please thank thanks between over across within having most each every one two three
|
|
53
|
+
""".split())
|
|
54
|
+
DATE_LINE = re.compile(r"^\s*(?:\d{1,2}(?:st|nd|rd|th)?\s+)?[A-Z][a-z]{2,8}\.?\s+(?:\d{1,2}(?:st|nd|rd|th)?,?\s+)?\d{4}\s*$")
|
|
55
|
+
# The joining words allowed inside a capitalised phrase: "Head of Geography".
|
|
56
|
+
JOINERS = {"of", "and", "for", "the", "in", "at", "&", "de"}
|
|
57
|
+
STOP = PLAIN | set("""
|
|
58
|
+
able across after also been before being between can could did does doing each every experience
|
|
59
|
+
experienced including into just like more most much need needs over role roles some such than
|
|
60
|
+
them through under using very well will within without work worked working would years year
|
|
61
|
+
have with that this from your their about must should strong good great team teams including
|
|
62
|
+
ability skills skill knowledge new help helps support supports ensure make makes
|
|
63
|
+
""".split())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read(path):
|
|
67
|
+
try:
|
|
68
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
69
|
+
return f.read()
|
|
70
|
+
except OSError as e:
|
|
71
|
+
print("claim_check: cannot read %s: %s" % (path, e.strerror), file=sys.stderr)
|
|
72
|
+
sys.exit(2)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def clean(line):
|
|
76
|
+
"""A line with its markup removed, so `**Go**` and `Go` read the same. Resumes kept as
|
|
77
|
+
Typst or LaTeX source escape `$`, `&`, `%` and `#`; the escapes are dropped too."""
|
|
78
|
+
line = LINK.sub(r"\1", line)
|
|
79
|
+
line = re.sub(r"\\([$&%#_])", r"\1", line)
|
|
80
|
+
line = re.sub(r"(?<![$£€\w.,])\d+(?:\.\d+)?(?:em|mm|pt|cm|px|fr)\b", " ", line) # layout lengths
|
|
81
|
+
line = line.replace("’", "'").replace("‘", "'").replace("–", " - ").replace("—", " - ")
|
|
82
|
+
return re.sub(r"[*_`~]", "", line)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def numbers(text):
|
|
86
|
+
"""{(value, is_percent)} for every figure in the text. "$2M" and "2 million" are one value."""
|
|
87
|
+
found = set()
|
|
88
|
+
for m in NUMBER.finditer(text):
|
|
89
|
+
try:
|
|
90
|
+
value = float(m.group(1).replace(",", ""))
|
|
91
|
+
except ValueError:
|
|
92
|
+
continue
|
|
93
|
+
unit = (m.group(2) or m.group(3) or "").lower()
|
|
94
|
+
found.add((value * MULT.get(unit, 1), bool(m.group(4))))
|
|
95
|
+
return found
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def stem(word):
|
|
99
|
+
w = word.lower().strip("'.")
|
|
100
|
+
if w.endswith("'s"):
|
|
101
|
+
w = w[:-2]
|
|
102
|
+
for s in SUFFIXES:
|
|
103
|
+
if w.endswith(s) and len(w) - len(s) >= 4:
|
|
104
|
+
return w[: -len(s)]
|
|
105
|
+
return w
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def key(word):
|
|
109
|
+
"""How a name is compared: case, dots, a plural, a possessive or a contraction don't matter."""
|
|
110
|
+
w = word.lower().strip("'")
|
|
111
|
+
w = re.sub(r"'(?:s|m|d|ve|ll|re|t)$", "", w)
|
|
112
|
+
w = w.replace(".", "") or w
|
|
113
|
+
return w[:-1] if len(w) > 4 and w.endswith("s") and not w.endswith("ss") else w
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def vocabulary(text):
|
|
117
|
+
names, stems = set(), set()
|
|
118
|
+
for line in text.splitlines():
|
|
119
|
+
for w in WORD.findall(clean(line)):
|
|
120
|
+
names.add(key(w))
|
|
121
|
+
stems.add(stem(w))
|
|
122
|
+
flat = " ".join(key(w) for line in text.splitlines() for w in WORD.findall(clean(line)))
|
|
123
|
+
return names, stems, " " + flat + " "
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def looks_named(word):
|
|
127
|
+
"""True for a word that is a name whatever its position: AWS, JavaScript, C++, k8s, .NET."""
|
|
128
|
+
body = word.lstrip(".")
|
|
129
|
+
return bool(re.search(r"[A-Z]", body[1:]) or re.search(r"[0-9+#]", body) or word.startswith("."))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def phrases(line):
|
|
133
|
+
"""Runs of capitalised or name-shaped words in one line, as lists of words.
|
|
134
|
+
|
|
135
|
+
The first word of a line or sentence is capitalised by grammar, not because it is a name,
|
|
136
|
+
so it only counts when it is name-shaped itself or opens a longer capitalised run."""
|
|
137
|
+
text = LIST_MARK.sub("", clean(line))
|
|
138
|
+
out, run, start_of_sentence = [], [], True
|
|
139
|
+
tokens = re.findall(r"(?<![A-Za-z0-9])\.?[A-Za-z][A-Za-z0-9+#']*(?:\.[A-Za-z0-9]+)*|&|[.!?;:]\s|[,|·/()]|\s-\s", text + " ")
|
|
140
|
+
for i, tok in enumerate(tokens):
|
|
141
|
+
if not (tok[0].isalpha() or tok[0] in ".&") or tok.strip() in (".", "!", "?", ";", ":"):
|
|
142
|
+
if run:
|
|
143
|
+
out.append(run)
|
|
144
|
+
run = []
|
|
145
|
+
start_of_sentence = tok.strip() in (".", "!", "?")
|
|
146
|
+
continue
|
|
147
|
+
if "'" in tok and key(tok) in PLAIN: # I'm, I've, we'd
|
|
148
|
+
tok = key(tok)
|
|
149
|
+
capital = tok[0].isupper() or looks_named(tok)
|
|
150
|
+
if capital and not (start_of_sentence and not looks_named(tok) and tok.lower() in PLAIN):
|
|
151
|
+
run.append((tok, start_of_sentence))
|
|
152
|
+
elif run and tok.lower() in JOINERS and i + 1 < len(tokens) and tokens[i + 1][:1].isupper():
|
|
153
|
+
run.append((tok, False))
|
|
154
|
+
else:
|
|
155
|
+
if run:
|
|
156
|
+
out.append(run)
|
|
157
|
+
run = []
|
|
158
|
+
start_of_sentence = False
|
|
159
|
+
if run:
|
|
160
|
+
out.append(run)
|
|
161
|
+
cleaned = []
|
|
162
|
+
for r in out:
|
|
163
|
+
# A lone sentence-opening word ("Led", "Built") is grammar, not a name.
|
|
164
|
+
if len(r) == 1 and r[0][1] and not looks_named(r[0][0]):
|
|
165
|
+
continue
|
|
166
|
+
# A sentence-opening verb in front of a name ("Led AWS migration") is dropped from the run.
|
|
167
|
+
if len(r) > 1 and r[0][1] and not looks_named(r[0][0]) and r[0][0].lower() not in PLAIN:
|
|
168
|
+
r = r[1:] if r[0][0].lower().endswith(("ed", "ing")) else r
|
|
169
|
+
words = [w for w, _ in r]
|
|
170
|
+
while words and words[-1].lower() in JOINERS:
|
|
171
|
+
words.pop()
|
|
172
|
+
if words:
|
|
173
|
+
cleaned.append(words)
|
|
174
|
+
return cleaned
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def check(draft, sources, posting):
|
|
178
|
+
src_names, src_stems, src_flat = vocabulary("\n".join(sources))
|
|
179
|
+
src_numbers = numbers("\n".join(clean(s) for s in sources))
|
|
180
|
+
post_names, post_stems, _ = vocabulary(posting or "")
|
|
181
|
+
post_numbers = numbers(clean(posting or ""))
|
|
182
|
+
findings, seen = [], set()
|
|
183
|
+
|
|
184
|
+
def add(kind, what, n, line, in_posting=False):
|
|
185
|
+
if (kind, what.lower()) in seen:
|
|
186
|
+
return
|
|
187
|
+
seen.add((kind, what.lower()))
|
|
188
|
+
findings.append({"kind": kind, "text": what, "line": n, "context": line.strip()[:160],
|
|
189
|
+
"in_posting": in_posting})
|
|
190
|
+
|
|
191
|
+
for n, raw in enumerate(draft.splitlines(), 1):
|
|
192
|
+
line = clean(raw)
|
|
193
|
+
if not line.strip() or DATE_LINE.match(LIST_MARK.sub("", line)):
|
|
194
|
+
continue # a letter's own date line claims nothing
|
|
195
|
+
body = LIST_MARK.sub("", line)
|
|
196
|
+
for m in NUMBER.finditer(body):
|
|
197
|
+
got = next(iter(numbers(m.group(0))), None)
|
|
198
|
+
if got and got not in src_numbers:
|
|
199
|
+
add("number", m.group(0).strip(), n, raw, got in post_numbers)
|
|
200
|
+
for words in phrases(raw):
|
|
201
|
+
content = [w for w in words if w.lower() not in JOINERS]
|
|
202
|
+
missing = [w for w in content if key(w) not in src_names and w.lower() not in PLAIN]
|
|
203
|
+
if missing:
|
|
204
|
+
add("name", " ".join(words), n, raw, all(key(w) in post_names for w in missing))
|
|
205
|
+
elif len(content) > 1 and all(w.lower() not in PLAIN for w in content):
|
|
206
|
+
flat = " ".join(key(w) for w in words if w != "&")
|
|
207
|
+
if " " + flat + " " not in src_flat:
|
|
208
|
+
add("combination", " ".join(words), n, raw)
|
|
209
|
+
if posting:
|
|
210
|
+
for w in WORD.findall(body):
|
|
211
|
+
s = stem(w)
|
|
212
|
+
if (len(s) >= 4 and w.lower() not in STOP and s in post_stems and s not in src_stems
|
|
213
|
+
and key(w) not in src_names and not w[0].isupper() and not looks_named(w)):
|
|
214
|
+
add("borrowed", w.lower(), n, raw, True)
|
|
215
|
+
return findings
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main():
|
|
219
|
+
ap = argparse.ArgumentParser(description="List what a draft says that the candidate's own material doesn't.")
|
|
220
|
+
ap.add_argument("draft")
|
|
221
|
+
ap.add_argument("--source", action="append", required=True, metavar="FILE",
|
|
222
|
+
help="the candidate's own resume or notes; repeat for more than one")
|
|
223
|
+
ap.add_argument("--posting", metavar="FILE", help="the job posting")
|
|
224
|
+
ap.add_argument("--json", action="store_true")
|
|
225
|
+
a = ap.parse_args()
|
|
226
|
+
|
|
227
|
+
findings = check(read(a.draft), [read(p) for p in a.source], read(a.posting) if a.posting else None)
|
|
228
|
+
if a.json:
|
|
229
|
+
print(json.dumps({"draft": a.draft, "findings": findings}, indent=1))
|
|
230
|
+
return 1 if findings else 0
|
|
231
|
+
if not findings:
|
|
232
|
+
print("PASS %s: every figure and name is in the source material." % a.draft)
|
|
233
|
+
return 0
|
|
234
|
+
labels = {"number": "Figures not in the source", "name": "Names not in the source",
|
|
235
|
+
"combination": "Words the source has, but not together",
|
|
236
|
+
"borrowed": "Wording taken from the posting"}
|
|
237
|
+
print("REVIEW %s: %d item(s) to check against the source material." % (a.draft, len(findings)))
|
|
238
|
+
for kind in ("number", "name", "combination", "borrowed"):
|
|
239
|
+
rows = [f for f in findings if f["kind"] == kind]
|
|
240
|
+
if not rows:
|
|
241
|
+
continue
|
|
242
|
+
print("\n%s (%d)" % (labels[kind], len(rows)))
|
|
243
|
+
for f in rows:
|
|
244
|
+
note = " [in posting]" if f["in_posting"] and kind != "borrowed" else ""
|
|
245
|
+
print(" line %d: %s%s\n %s" % (f["line"], f["text"], note, f["context"]))
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|
|
@@ -14,6 +14,36 @@
|
|
|
14
14
|
"Includes at least three runnable sanity checks, including a known-answer fixture or a reconciliation with billing",
|
|
15
15
|
"Does not invent tables, columns or an owner not given in the prompt; anything assumed is tagged [assumption] and listed under open decisions"
|
|
16
16
|
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": 2,
|
|
20
|
+
"held_out": "2026-10-05",
|
|
21
|
+
"prompt": "Finance and CX keep turning up to the Monday meeting with different refund rates and it's getting silly. I've pasted the Slack thread into thread.md and the table descriptions into tables.md. Can you write down one proper definition of refund rate that we can all use, with the SQL? We're on Postgres.",
|
|
22
|
+
"assertions": [
|
|
23
|
+
"It separates the share of orders refunded from the share of revenue refunded as two differently named measures, or picks one as 'refund rate' and names the other as a separate measure; it does not blend them into one formula",
|
|
24
|
+
"It points out that 312 refunded orders out of 7,480 is about 4.2%, not the 4.6% quoted in the thread",
|
|
25
|
+
"The order-based SQL counts each refunded order once (COUNT DISTINCT on order_id, EXISTS, or refunds aggregated per order before joining), so orders with several refund rows are not double-counted",
|
|
26
|
+
"It states one rule for which month a refund belongs to (the order's month or the refund's month) and says what happens to a February order refunded in March",
|
|
27
|
+
"It says a single money-based rate across both stores cannot be computed from these tables as they stand because there is no exchange-rate data, and it does not invent an exchange rate or an exchange-rate table",
|
|
28
|
+
"Test orders are excluded with a condition on orders.is_test, and staff orders are flagged as not identifiable from the listed columns; it does not use a staff flag column that does not exist",
|
|
29
|
+
"Month boundaries are set in UK time (Europe/London) against timestamps stored in UTC",
|
|
30
|
+
"It lists whether cancelled orders count in the denominator and who owns the metric as open decisions, without naming an owner the thread did not name"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 3,
|
|
35
|
+
"held_out": "2026-10-05",
|
|
36
|
+
"prompt": "need a proper definition of 'time to first response' for our support team. we use a helpdesk tool and I can export tickets to a spreadsheet. my boss wants it on the weekly report. there's 8 agents. not sure if weekends should count, we don't work them",
|
|
37
|
+
"assertions": [
|
|
38
|
+
"It delivers a written definition in the reply and does not stop at a list of questions",
|
|
39
|
+
"It defines the start and the end of the clock (ticket created to the first reply from a human agent) and says automatic acknowledgement emails do not count as the first response, or flags that as a decision",
|
|
40
|
+
"Whether weekends and out-of-hours time count is given as an open decision with a recommended default or with both versions described; it is not decided silently",
|
|
41
|
+
"It says which statistic goes on the report (median, mean or a percentile), not just 'average' left undefined",
|
|
42
|
+
"It says how tickets that have had no reply yet when the report is run are treated",
|
|
43
|
+
"It says which week a ticket is counted in (the week it was created or the week it was answered)",
|
|
44
|
+
"Any export column names it uses are marked as assumed, since no export layout was given",
|
|
45
|
+
"It does not state a response-time target or SLA figure as the team's own, since none was given"
|
|
46
|
+
]
|
|
17
47
|
}
|
|
18
48
|
]
|
|
19
49
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Tallow & Thyme: tables (Postgres 15)
|
|
2
|
+
|
|
3
|
+
All timestamps are `timestamptz`, stored in UTC. The business reports on UK time.
|
|
4
|
+
|
|
5
|
+
## orders
|
|
6
|
+
|
|
7
|
+
| Column | Type | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| order_id | bigint | primary key |
|
|
10
|
+
| customer_id | bigint | references customers |
|
|
11
|
+
| store | text | 'uk' or 'eu' |
|
|
12
|
+
| currency | text | 'GBP' for the uk store, 'EUR' for the eu store |
|
|
13
|
+
| gross_amount_minor | integer | order total in pence (GBP) or cents (EUR) |
|
|
14
|
+
| placed_at | timestamptz | when the order was placed |
|
|
15
|
+
| status | text | 'placed', 'delivered', 'cancelled' |
|
|
16
|
+
| is_test | boolean | true for orders made by the test harness |
|
|
17
|
+
|
|
18
|
+
## refunds
|
|
19
|
+
|
|
20
|
+
| Column | Type | Notes |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| refund_id | bigint | primary key |
|
|
23
|
+
| order_id | bigint | references orders |
|
|
24
|
+
| amount_minor | integer | in the same currency as the order |
|
|
25
|
+
| reason | text | free text typed by the CX agent |
|
|
26
|
+
| created_at | timestamptz | when the refund was issued |
|
|
27
|
+
|
|
28
|
+
An order can have more than one refund row: a missing ingredient is refunded, then a second
|
|
29
|
+
problem with the same box is refunded later. About 1 in 9 refunded orders has two or more
|
|
30
|
+
rows.
|
|
31
|
+
|
|
32
|
+
## customers
|
|
33
|
+
|
|
34
|
+
| Column | Type | Notes |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| customer_id | bigint | primary key |
|
|
37
|
+
| email | text | |
|
|
38
|
+
| created_at | timestamptz | |
|
|
39
|
+
|
|
40
|
+
## Not in the database
|
|
41
|
+
|
|
42
|
+
There is no exchange-rate table. Finance converts EUR to GBP by hand in a spreadsheet.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# #ops-metrics, exported 3 March 2026
|
|
2
|
+
|
|
3
|
+
**Hattie Blum** (CX lead) 09:12
|
|
4
|
+
Feb refund rate is 4.6%. 312 refunded orders out of 7,480 orders.
|
|
5
|
+
|
|
6
|
+
**Rupert Osei** (Finance) 09:20
|
|
7
|
+
I get 3.5%. £9,870 refunded on £284,300 gross.
|
|
8
|
+
|
|
9
|
+
**Hattie Blum** 09:24
|
|
10
|
+
Well mine is orders, yours is money. I count an order as refunded if it shows up in the
|
|
11
|
+
refunds table. I go by the date the refund was issued, so that's refunds issued in Feb.
|
|
12
|
+
|
|
13
|
+
**Rupert Osei** 09:31
|
|
14
|
+
I go by the month the order was placed. If a Feb order gets refunded in March it goes back
|
|
15
|
+
against Feb, that's how the P&L works. Also mine is the UK store only. The EU store is in a
|
|
16
|
+
separate sheet and I convert the euros by hand at whatever the rate is that day, I haven't
|
|
17
|
+
done Feb yet.
|
|
18
|
+
|
|
19
|
+
**Hattie Blum** 09:33
|
|
20
|
+
Mine is both stores.
|
|
21
|
+
|
|
22
|
+
**Mina Castellanos** (founder) 09:47
|
|
23
|
+
This is why the Monday meeting goes round in circles. I want ONE refund rate for the whole
|
|
24
|
+
business, both stores, monthly, on UK time. And take out test orders, and staff orders too.
|
|
25
|
+
Staff get free boxes and we refund them through the system so they make it look worse than
|
|
26
|
+
it is.
|
|
27
|
+
|
|
28
|
+
**Hattie Blum** 09:52
|
|
29
|
+
There's an is_test flag on orders. No idea how you'd spot staff orders though, we just know
|
|
30
|
+
the names.
|
|
31
|
+
|
|
32
|
+
**Rupert Osei** 09:55
|
|
33
|
+
Do cancelled orders count as orders? I leave them in, they were placed.
|
|
34
|
+
|
|
35
|
+
**Hattie Blum** 09:58
|
|
36
|
+
I take them out.
|
|
37
|
+
|
|
38
|
+
**Mina Castellanos** 10:05
|
|
39
|
+
Someone write it down properly please. Who owns it after that, tbd, let's get the definition
|
|
40
|
+
sorted first.
|
|
@@ -14,6 +14,35 @@
|
|
|
14
14
|
"Ends with next steps that place files under packs/sre-oncall/skills/sla-credit-calculator/ and require python3 validate.py to pass before opening a PR",
|
|
15
15
|
"Does not invent inputs, deps, or related skills the author did not give"
|
|
16
16
|
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": 2,
|
|
20
|
+
"held_out": "2026-10-05",
|
|
21
|
+
"prompt": "I want to add a skill to our team's skills repo. The repo is this folder (CONTRIBUTING.md, AGENTS.md, existing skills under skills/). The new one is carrier-invoice-audit: you give it a carrier's freight invoice and the rate confirmation for that load, and it lists every line where the carrier billed more than we agreed, with the difference. Ignore differences of $5.00 or less per line. It goes under billing. Related skills are rate-con-extractor and bol-parser. And if it finds a detention charge on the invoice it should kick off detention-claim-letter automatically at the end. Write everything I need for the pull request.",
|
|
22
|
+
"assertions": [
|
|
23
|
+
"The skill is delivered as skills/carrier-invoice-audit/ containing skill.yaml, INSTRUCTIONS.md and tests/cases.yaml, the layout CONTRIBUTING.md requires, and not as some other layout such as manifest.json plus SKILL.md",
|
|
24
|
+
"The related list contains rate-con-extractor and does not contain bol-parser, and the reply says bol-parser is not a skill in the repo",
|
|
25
|
+
"The skill's instructions do not make it run or hand off to detention-claim-letter, and the reply says the repo's rule that a skill never calls another skill is the reason",
|
|
26
|
+
"The owner field is a team that appears in OWNERS.md (billing-ops, dispatch or claims), and the reply tells the author the owner was not given and needs confirming",
|
|
27
|
+
"tests/cases.yaml has at least five cases, one of which covers a missing input such as no rate confirmation supplied, following AGENTS.md's rule over the three cases CONTRIBUTING.md asks for",
|
|
28
|
+
"The $5.00 per-line tolerance appears as an exact figure in INSTRUCTIONS.md, and no other threshold or percentage is introduced",
|
|
29
|
+
"A CATALOG.md row for carrier-invoice-audit is provided for the billing table",
|
|
30
|
+
"The reply tells the author to run make check before opening the pull request"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 3,
|
|
35
|
+
"held_out": "2026-10-05",
|
|
36
|
+
"prompt": "need a new skill for our skills repo. takes the standup notes people paste from slack during the week and writes the friday update that goes to our director. not sure on the name yet, weekly-digest or friday-rollup. Priya wanted it to pull ticket status from Jira too but we don't have API access sorted so leave that out for now i guess. don't know which folder it should live in, whoever set the repo up is on leave. can you write the skill up plus a few test cases so i can open a PR when she's back",
|
|
37
|
+
"assertions": [
|
|
38
|
+
"It delivers a drafted instruction file for the skill and at least three test cases, and does not stop at a list of questions",
|
|
39
|
+
"It says the repo's conventions (required files, metadata format or folder) were not available and that the file layout used is an assumption to check",
|
|
40
|
+
"It uses one name for the skill throughout and tells the author the choice between weekly-digest and friday-rollup is still theirs to make",
|
|
41
|
+
"The skill as written does not fetch from Jira and declares no Jira credential, API key or dependency; Jira is mentioned only as left out for now",
|
|
42
|
+
"The skill's instructions tell it to report only what is in the pasted standup notes and not to add progress, blockers or ticket status the notes do not contain",
|
|
43
|
+
"It does not invent the director's name, the team's name, team members other than Priya, or a specific repo folder stated as fact",
|
|
44
|
+
"The test cases use realistic pasted standup notes as input, and at least one covers thin or missing notes"
|
|
45
|
+
]
|
|
17
46
|
}
|
|
18
47
|
]
|
|
19
48
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Instructions for agents working in this repo
|
|
2
|
+
|
|
3
|
+
Read `CONTRIBUTING.md` first. The rules below were added after it and win where the two differ.
|
|
4
|
+
|
|
5
|
+
1. **A skill never calls, starts or hands off to another skill.** Each folder has to work when it is the only one installed. If a second skill is the natural follow-up, say so to the user in the final line of the output and let them run it.
|
|
6
|
+
2. **Five test cases minimum, not three.** At least one case must cover missing input: the user leaves out something listed under `inputs`, and the expected behaviour is that the skill says what is missing and does not guess.
|
|
7
|
+
3. **No customer or carrier names from real accounts** in instructions or tests. Use made-up ones.
|
|
8
|
+
4. **Money rules are written as numbers.** If a skill applies a threshold or tolerance, the exact figure goes in `INSTRUCTIONS.md`. "A reasonable tolerance" is not acceptable.
|
|
9
|
+
5. Do not edit `OWNERS.md`. If no listed team fits, ask in the pull request.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Skill catalogue
|
|
2
|
+
|
|
3
|
+
## billing
|
|
4
|
+
|
|
5
|
+
| skill | what it does | owner |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [`rate-con-extractor`](skills/rate-con-extractor/INSTRUCTIONS.md) | Pulls the agreed rate, fuel surcharge and accessorials out of a rate confirmation into a table. | billing-ops |
|
|
8
|
+
|
|
9
|
+
## dispatch
|
|
10
|
+
|
|
11
|
+
| skill | what it does | owner |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
|
|
14
|
+
## claims
|
|
15
|
+
|
|
16
|
+
| skill | what it does | owner |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| [`detention-claim-letter`](skills/detention-claim-letter/INSTRUCTIONS.md) | Drafts a detention claim letter to a shipper or receiver from the gate timestamps and the free-time terms. | claims |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Contributing to larkfield-agent-skills
|
|
2
|
+
|
|
3
|
+
Internal skills for the Larkfield Freight operations and billing teams. One folder per skill under `skills/`.
|
|
4
|
+
|
|
5
|
+
## What a skill folder contains
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
skills/<skill-name>/
|
|
9
|
+
skill.yaml # metadata, see below
|
|
10
|
+
INSTRUCTIONS.md # what the agent does
|
|
11
|
+
tests/cases.yaml # test cases
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`<skill-name>` is lowercase kebab-case and matches `name` in `skill.yaml`.
|
|
15
|
+
|
|
16
|
+
## skill.yaml
|
|
17
|
+
|
|
18
|
+
All of these are required:
|
|
19
|
+
|
|
20
|
+
| field | notes |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `name` | same as the folder |
|
|
23
|
+
| `summary` | one sentence, 160 characters or fewer |
|
|
24
|
+
| `owner` | a team listed in `OWNERS.md` — the team that answers when the skill is wrong |
|
|
25
|
+
| `category` | `billing`, `dispatch` or `claims` |
|
|
26
|
+
| `inputs` | list of what the user must supply |
|
|
27
|
+
| `related` | list of other skills in this repo (may be empty). Every entry must be a folder under `skills/` |
|
|
28
|
+
| `network` | `false` unless the security team has signed off |
|
|
29
|
+
|
|
30
|
+
## INSTRUCTIONS.md
|
|
31
|
+
|
|
32
|
+
Three sections, in this order: `## Purpose`, `## Steps`, `## Never`.
|
|
33
|
+
`## Never` lists at least two things the skill must not do.
|
|
34
|
+
|
|
35
|
+
## Tests
|
|
36
|
+
|
|
37
|
+
`tests/cases.yaml` needs at least 3 cases. Each case has `name`, `input` and `expect` (a list of things that must be true of the output).
|
|
38
|
+
|
|
39
|
+
## Catalogue
|
|
40
|
+
|
|
41
|
+
Add one row for the skill to `CATALOG.md`, in the table for its category, keeping the rows in alphabetical order.
|
|
42
|
+
|
|
43
|
+
## Before you open a pull request
|
|
44
|
+
|
|
45
|
+
Run `make check`. It validates every `skill.yaml`, checks that `related` entries exist and counts the test cases. A pull request with a failing `make check` is not reviewed.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Owning teams
|
|
2
|
+
|
|
3
|
+
| team | covers | contact |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| `billing-ops` | carrier payables, customer invoicing | Ines Okonkwo-Reyes |
|
|
6
|
+
| `dispatch` | load planning, carrier assignment | Tobias Vrana |
|
|
7
|
+
| `claims` | detention, damage and shortage claims | Marguerite Dlamini |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# detention-claim-letter
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
When a driver is held at a facility past the agreed free time, Larkfield claims detention from the party that caused the delay. This skill drafts that letter with the arithmetic shown so the recipient can check it.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Work out time on site: departure minus arrival, in hours and minutes.
|
|
10
|
+
2. Subtract the free time. If the result is zero or less, say that no detention is owed and stop.
|
|
11
|
+
3. Bill the remainder in the increments the terms state. If the terms do not state an increment, bill in 15-minute increments rounded down.
|
|
12
|
+
4. Apply the cap if the terms state one.
|
|
13
|
+
5. Write the letter: load number, facility, the timestamps, the calculation line by line, the amount claimed, and a reply-by date 14 days from the letter date.
|
|
14
|
+
6. End the output with one line listing what evidence to attach (the signed bill of lading with in and out times).
|
|
15
|
+
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
- Never claim an amount without showing the timestamps it came from.
|
|
19
|
+
- Never round time on site up.
|
|
20
|
+
- Never state that payment is overdue; this is a first claim.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: detention-claim-letter
|
|
2
|
+
summary: Drafts a detention claim letter to a shipper or receiver from the gate timestamps and the free-time terms.
|
|
3
|
+
owner: claims
|
|
4
|
+
category: claims
|
|
5
|
+
inputs:
|
|
6
|
+
- arrival and departure timestamps at the facility
|
|
7
|
+
- the free time and hourly detention rate that apply
|
|
8
|
+
- who the letter is addressed to
|
|
9
|
+
related:
|
|
10
|
+
- rate-con-extractor
|
|
11
|
+
network: false
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
- name: three-hours-over
|
|
2
|
+
input: Arrived 07:10, departed 12:10 at Halvorsen Cold Storage. 2 hours free, $60/hr. Load LF-207733. Address it to Halvorsen's receiving manager.
|
|
3
|
+
expect:
|
|
4
|
+
- time on site is 5 hours and billable time is 3 hours
|
|
5
|
+
- the amount claimed is 180.00
|
|
6
|
+
- the reply-by date is 14 days after the letter date
|
|
7
|
+
- name: inside-free-time
|
|
8
|
+
input: Arrived 09:00, departed 10:45. 2 hours free, $75/hr. Load LF-207801, to Pellham Distribution.
|
|
9
|
+
expect:
|
|
10
|
+
- the reply says no detention is owed
|
|
11
|
+
- no letter is drafted
|
|
12
|
+
- name: cap-applies
|
|
13
|
+
input: Arrived 06:00, departed 17:30 at Norcastle Foods. 2 hours free, $60/hr, max $480. Load LF-207950.
|
|
14
|
+
expect:
|
|
15
|
+
- the uncapped figure 570.00 is shown and the claim is 480.00
|
|
16
|
+
- name: partial-increment
|
|
17
|
+
input: Arrived 13:05, departed 16:25. 2 hours free, $60/hr. Load LF-208010, to Wexcombe Mills.
|
|
18
|
+
expect:
|
|
19
|
+
- billable time is 1 hour 15 minutes, not 1 hour 20 minutes
|
|
20
|
+
- the amount claimed is 75.00
|
|
21
|
+
- name: no-rate-given
|
|
22
|
+
input: Driver sat at Halvorsen for six hours yesterday on LF-208122, write the detention letter.
|
|
23
|
+
expect:
|
|
24
|
+
- the reply says the timestamps, the free time and the hourly rate are missing
|
|
25
|
+
- no amount is claimed
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# rate-con-extractor
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A rate confirmation is the agreement between Larkfield and a carrier for one load. Billing needs its numbers in a fixed shape before anything can be checked against it. This skill reads one rate confirmation and returns those numbers as a table.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Find the load number, the carrier name, the pickup and delivery dates.
|
|
10
|
+
2. Find the linehaul rate. Record whether it is a flat amount or a per-mile rate, and the miles if per-mile.
|
|
11
|
+
3. Find the fuel surcharge. Record it as written: a percentage of linehaul, a per-mile amount, or a flat amount.
|
|
12
|
+
4. List every accessorial the document names (detention, lumper, layover, stop-off, tarp) with its rate and any free time or cap.
|
|
13
|
+
5. Return one table: field, value, and the line of the document the value came from.
|
|
14
|
+
6. List any field in steps 1 to 4 that the document does not state, under "Not stated".
|
|
15
|
+
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
- Never calculate a total. This skill extracts; it does not add up.
|
|
19
|
+
- Never fill a missing field from what is usual for the lane or the carrier.
|
|
20
|
+
- Never merge two rate confirmations into one table.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
- name: flat-linehaul-percent-fuel
|
|
2
|
+
input: |
|
|
3
|
+
RATE CONFIRMATION Load LF-208841 Carrier: Brightwater Haulage
|
|
4
|
+
Pickup 03/04 Delivery 03/06
|
|
5
|
+
Linehaul: $1,850.00 flat Fuel surcharge: 18% of linehaul
|
|
6
|
+
Detention: $60/hr after 2 hrs free, max $480
|
|
7
|
+
expect:
|
|
8
|
+
- the table has linehaul 1850.00 marked flat
|
|
9
|
+
- fuel surcharge is recorded as 18% of linehaul, not as a dollar amount
|
|
10
|
+
- detention is listed with 60 per hour, 2 hours free and the 480 cap
|
|
11
|
+
- name: per-mile-linehaul
|
|
12
|
+
input: |
|
|
13
|
+
Load LF-209112 Carrier: Copperline Transport
|
|
14
|
+
Linehaul $2.35/mi x 612 mi FSC $0.48/mi
|
|
15
|
+
expect:
|
|
16
|
+
- linehaul is recorded as 2.35 per mile with 612 miles
|
|
17
|
+
- no total is calculated
|
|
18
|
+
- name: no-fuel-surcharge-stated
|
|
19
|
+
input: |
|
|
20
|
+
Load LF-209377 Carrier: Ashgrove Carriers Linehaul $940 flat Lumper: reimbursed with receipt
|
|
21
|
+
expect:
|
|
22
|
+
- fuel surcharge appears under Not stated
|
|
23
|
+
- no fuel surcharge figure is given
|
|
24
|
+
- name: two-documents-pasted
|
|
25
|
+
input: two rate confirmations pasted one after the other, loads LF-209400 and LF-209401
|
|
26
|
+
expect:
|
|
27
|
+
- the output is two separate tables or a request to send one at a time
|
|
28
|
+
- name: nothing-supplied
|
|
29
|
+
input: Extract the rate con for load LF-209500.
|
|
30
|
+
expect:
|
|
31
|
+
- the reply says the rate confirmation text is missing
|
|
32
|
+
- no rates are given
|