knowme-careerforge-skill 0.0.7 → 0.0.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/AGENT.md +7 -6
- package/ARCHITECTURE.md +18 -15
- package/README.md +23 -15
- package/README.zh-CN.md +23 -15
- package/SKILL.md +26 -33
- package/agents/codex/knowme-careerforge.yaml +1 -1
- package/agents/opencode/skill.yaml +1 -1
- package/dist/cli/src/index.js +22 -14
- package/package.json +1 -1
- package/references/01-evidence-mining.md +80 -3
- package/references/02-career-goal.md +35 -15
- package/references/03-jd-analysis.md +10 -0
- package/references/04-template-selection.md +7 -2
- package/references/05-html-canvas-tokens.md +4 -2
- package/references/06-qa-and-rendering.md +39 -5
- package/references/07-artifact-contract.md +21 -7
- package/scripts/Agent.md +3 -3
- package/scripts/build/build-gallery.py +2 -1
- package/scripts/build/release.py +3 -2
- package/scripts/evidence/analyze-jd.py +49 -8
- package/scripts/evidence/extract-evidence.py +17 -391
- package/scripts/pipeline/forge.py +159 -85
- package/scripts/rendering/browser-engine.js +5 -1
- package/scripts/template/instantiate-resume.py +5 -3
- package/skill.json +1 -1
- package/src/knowledge/career/seniority-signals.json +1 -0
- package/src/knowledge/index.json +176 -2
- package/src/knowledge/resume/action-verbs.json +3 -2
- package/src/knowledge/resume/fab-model.json +5 -4
- package/src/knowledge/resume-schema.json +1 -1
- package/src/knowledge/templates.json +174 -0
- package/src/references/01-evidence-mining.md +80 -3
- package/src/references/02-career-goal.md +35 -15
- package/src/references/03-jd-analysis.md +10 -0
- package/src/references/04-template-selection.md +7 -2
- package/src/references/05-html-canvas-tokens.md +4 -2
- package/src/references/06-qa-and-rendering.md +39 -5
- package/src/references/07-artifact-contract.md +21 -7
- package/src/templates/academic-research/canvas.html +2 -1
- package/src/templates/classic/canvas.html +2 -1
- package/src/templates/classic/style.css +90 -28
- package/src/templates/common/base.css +19 -3
- package/src/templates/common/canvas-bindings.css +247 -12
- package/src/templates/compact-dense/style.css +5 -1
- package/src/templates/creative-tech/style.css +30 -1
- package/src/templates/data-analyst/style.css +31 -1
- package/src/templates/editorial/README.md +7 -0
- package/src/templates/editorial/canvas.html +22 -0
- package/src/templates/editorial/metadata.json +36 -0
- package/src/templates/editorial/sample-profile.json +86 -0
- package/src/templates/editorial/style.css +24 -0
- package/src/templates/executive/style.css +52 -18
- package/src/templates/graduate/README.md +7 -0
- package/src/templates/graduate/canvas.html +22 -0
- package/src/templates/graduate/metadata.json +38 -0
- package/src/templates/graduate/sample-profile.json +60 -0
- package/src/templates/graduate/style.css +24 -0
- package/src/templates/international-flow/canvas.html +2 -1
- package/src/templates/minimal/canvas.html +3 -2
- package/src/templates/minimal/style.css +6 -0
- package/src/templates/modern/style.css +43 -55
- package/src/templates/project-focus/README.md +7 -0
- package/src/templates/project-focus/canvas.html +22 -0
- package/src/templates/project-focus/metadata.json +36 -0
- package/src/templates/project-focus/sample-profile.json +93 -0
- package/src/templates/project-focus/style.css +24 -0
- package/src/templates/startup-generalist/canvas.html +2 -1
- package/src/templates/startup-generalist/style.css +15 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Build a verified resume in isolation; retain only delivery files unless archiving is requested."""
|
|
3
3
|
import argparse
|
|
4
4
|
import contextlib
|
|
5
5
|
import copy
|
|
@@ -53,7 +53,7 @@ def file_hash(path):
|
|
|
53
53
|
def publish_aliases(pairs):
|
|
54
54
|
"""Stage every requested copy before publishing; rollback ordinary write failures.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Source bytes have already passed QA. Lock files reject concurrent publishers
|
|
57
57
|
to the same requested destination instead of interleaving their outputs.
|
|
58
58
|
"""
|
|
59
59
|
staged, locks, replaced = [], [], []
|
|
@@ -97,8 +97,11 @@ class Parser(argparse.ArgumentParser):
|
|
|
97
97
|
|
|
98
98
|
def main():
|
|
99
99
|
parser = Parser(description=__doc__)
|
|
100
|
-
parser.
|
|
101
|
-
|
|
100
|
+
inputs = parser.add_mutually_exclusive_group(required=True)
|
|
101
|
+
inputs.add_argument('--profile-json', help='User/Agent facts file, or - to read JSON from stdin without saving an input file')
|
|
102
|
+
inputs.add_argument('--html-input', help='Existing edited canvas file, or - for HTML on stdin; validates and publishes the pair without reconstructing a profile')
|
|
103
|
+
parser.add_argument('--workspace', help='Explicit archive parent; implies --keep-workspace (legacy behavior)')
|
|
104
|
+
parser.add_argument('--keep-workspace', action='store_true', help='Retain full diagnostic runs under --workspace or workspace/runs; default uses temporary staging')
|
|
102
105
|
parser.add_argument('--draft', action='store_true', help='Create an incomplete draft canvas without claiming PDF delivery')
|
|
103
106
|
parser.add_argument('--role')
|
|
104
107
|
parser.add_argument('--jd', help='Optional JD path or inline text; used only as highlighting/search hints')
|
|
@@ -106,95 +109,133 @@ def main():
|
|
|
106
109
|
parser.add_argument('--font-preset', choices=('system', 'arial-unicode'), default='system', help='Explicit local font choice; missing fonts fail verification')
|
|
107
110
|
for key in ('name', 'email', 'phone'):
|
|
108
111
|
parser.add_argument('--' + key)
|
|
109
|
-
parser.add_argument('--output', '-o', help='
|
|
110
|
-
parser.add_argument('--html-output', help='
|
|
112
|
+
parser.add_argument('--output', '-o', help='Verified PDF destination; default resume/resume.pdf in clean mode')
|
|
113
|
+
parser.add_argument('--html-output', help='HTML destination; default beside the PDF, or resume/draft.html for Draft')
|
|
111
114
|
parser.add_argument('--expected-pages', type=int, choices=(1, 2), default=1)
|
|
112
115
|
parser.add_argument('--auto-heal', action='store_true')
|
|
113
116
|
parser.add_argument('--quiet', '-q', action='store_true')
|
|
114
|
-
parser.add_argument('--summary', action='store_true', help='Compact report
|
|
117
|
+
parser.add_argument('--summary', action='store_true', help='Compact report (default in clean mode); manifest exists only for archived runs')
|
|
115
118
|
args = parser.parse_args()
|
|
119
|
+
if (args.profile_json == '-' or args.html_input == '-') and sys.stdin.isatty():
|
|
120
|
+
parser.error('Stdin input is required; pipe the prepared JSON/HTML or provide an input file')
|
|
121
|
+
if args.draft and args.output:
|
|
122
|
+
parser.error('--output is a PDF destination and cannot be used with --draft')
|
|
123
|
+
if args.html_input is not None and (any(getattr(args, key) is not None for key in ('role', 'jd', 'template', 'name', 'email', 'phone')) or args.font_preset != 'system'):
|
|
124
|
+
parser.error('Existing HTML keeps its own content, template and fonts; edit the canvas instead of passing profile overrides')
|
|
125
|
+
retained = args.keep_workspace or args.workspace is not None
|
|
126
|
+
try:
|
|
127
|
+
if not retained:
|
|
128
|
+
if args.draft:
|
|
129
|
+
args.html_output = args.html_output or 'resume/draft.html'
|
|
130
|
+
else:
|
|
131
|
+
args.output = args.output or str(Path(args.html_output).with_suffix('.pdf') if args.html_output else Path('resume/resume.pdf'))
|
|
132
|
+
args.html_output = args.html_output or str(Path(args.output).with_suffix('.html'))
|
|
133
|
+
except ValueError as error:
|
|
134
|
+
parser.error(str(error))
|
|
116
135
|
report = {'schemaVersion': '1.0', 'runId': uuid.uuid4().hex, 'status': 'RUNNING',
|
|
117
136
|
'stage': 'input', 'errors': [], 'warnings': [], 'checks': {}, 'outputs': {}}
|
|
118
137
|
run = None
|
|
138
|
+
temporary = None
|
|
119
139
|
exit_code = 1
|
|
120
140
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
if retained:
|
|
142
|
+
parent = Path(args.workspace or 'workspace/runs').resolve()
|
|
143
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
run = parent / report['runId']
|
|
145
|
+
run.mkdir(mode=0o700)
|
|
146
|
+
else:
|
|
147
|
+
temporary = tempfile.TemporaryDirectory(prefix='knowme-run-')
|
|
148
|
+
run = Path(temporary.name).resolve()
|
|
149
|
+
report['retention'] = 'archived' if retained else 'temporary'
|
|
150
|
+
report['runDirectory'] = str(run) if retained else None
|
|
126
151
|
report['startedAt'] = datetime.now(timezone.utc).isoformat()
|
|
127
152
|
write_json(run / 'manifest.json', report)
|
|
128
|
-
|
|
153
|
+
input_arg = args.profile_json if args.profile_json is not None else args.html_input
|
|
154
|
+
source = None if input_arg == '-' else Path(input_arg).resolve()
|
|
129
155
|
aliases = [Path(p).resolve() for p in (args.html_output, args.output) if p]
|
|
130
|
-
|
|
156
|
+
paths = ([source] if source is not None else []) + aliases
|
|
157
|
+
if len(set(paths)) != len(paths):
|
|
131
158
|
raise ValueError('Input, HTML and PDF output paths must be distinct')
|
|
132
159
|
for target in aliases:
|
|
133
160
|
if target == run or run in target.parents:
|
|
134
161
|
raise ValueError('Explicit output cannot replace an internal run artifact')
|
|
135
|
-
raw_bytes = source.read_bytes()
|
|
136
|
-
atomic_write(run / 'input.json', raw_bytes)
|
|
137
|
-
supplied = contract.load_document(json.loads(raw_bytes))
|
|
138
|
-
if supplied['kind'] == 'variant':
|
|
139
|
-
raise ValueError('A Variant is not a Master; use its original Master to derive another resume')
|
|
140
|
-
if supplied['kind'] == 'draft' and not args.draft:
|
|
141
|
-
raise ValueError('Draft input cannot be delivered; review facts and provide a Master, or use --draft')
|
|
142
|
-
facts = copy.deepcopy(supplied['profile'])
|
|
143
|
-
for key in ('name', 'email', 'phone'):
|
|
144
|
-
if getattr(args, key) is not None:
|
|
145
|
-
facts['basics'][key] = getattr(args, key)
|
|
146
|
-
kind = 'draft' if args.draft else 'master'
|
|
147
|
-
master = contract.document(kind, facts, {'type': 'user-input', 'path': str(source),
|
|
148
|
-
'inputSha256': hashlib.sha256(raw_bytes).hexdigest(),
|
|
149
|
-
'suppliedKind': supplied['kind'], 'suppliedSource': supplied['source']})
|
|
150
|
-
write_json(run / (kind + '.json'), master)
|
|
151
|
-
report['checks']['missingFields'] = master['missingFields']
|
|
152
|
-
report['warnings'].extend('Missing optional contact/position field: ' + v for v in master['missingFields'] if v != 'basics.name')
|
|
153
|
-
if not args.draft:
|
|
154
|
-
errors = contract.delivery_errors(master['profile'])
|
|
155
|
-
if errors:
|
|
156
|
-
raise ValueError('; '.join(errors))
|
|
157
|
-
profile = copy.deepcopy(master['profile'])
|
|
158
|
-
if args.role is not None:
|
|
159
|
-
profile['basics']['title'] = args.role
|
|
160
|
-
keywords = []
|
|
161
|
-
if args.jd:
|
|
162
|
-
report['stage'] = 'jd'
|
|
163
|
-
try:
|
|
164
|
-
jd_file = Path(args.jd)
|
|
165
|
-
jd = jd_file.read_text(encoding='utf-8') if jd_file.is_file() else args.jd
|
|
166
|
-
except OSError:
|
|
167
|
-
jd = args.jd
|
|
168
|
-
atomic_write(run / 'jd.txt', jd)
|
|
169
|
-
analyzer = load_module('jd_analyzer', 'evidence/analyze-jd.py')
|
|
170
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
171
|
-
result = analyzer.analyze_jd_text(jd)
|
|
172
|
-
write_json(run / 'jd-analysis.json', result)
|
|
173
|
-
keywords = result.get('detectedSkills', [])
|
|
174
|
-
report['stage'] = 'template'
|
|
175
|
-
template = args.template
|
|
176
|
-
if not template:
|
|
177
|
-
search = load_module('template_search', 'template/search-template.py')
|
|
178
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
179
|
-
results = search.search_templates(role=args.role or profile['basics'].get('title') or '',
|
|
180
|
-
keywords=','.join(keywords), target_pages=args.expected_pages, engine='hybrid')
|
|
181
|
-
if not results:
|
|
182
|
-
raise ValueError('No matching template available')
|
|
183
|
-
template = results[0].get('id') or results[0]['template']['id']
|
|
184
|
-
variant = contract.document('draft' if args.draft else 'variant', profile,
|
|
185
|
-
{'masterSha256': master['profileSha256'], 'masterPath': str(run / (kind + '.json')),
|
|
186
|
-
'template': template, 'targetRole': args.role, 'fontPreset': args.font_preset})
|
|
187
|
-
variant_path = run / ('draft-canvas-profile.json' if args.draft else 'variant.json')
|
|
188
|
-
write_json(variant_path, variant)
|
|
189
|
-
report['templateUsed'] = template
|
|
190
|
-
report['fontPreset'] = args.font_preset
|
|
191
|
-
report['stage'] = 'binding'
|
|
192
|
-
inst = load_module('instantiate_resume', 'template/instantiate-resume.py')
|
|
162
|
+
raw_bytes = sys.stdin.read().encode('utf-8') if source is None else source.read_bytes()
|
|
193
163
|
canvas, pdf = run / 'resume.html', run / 'resume.pdf'
|
|
194
|
-
|
|
195
|
-
|
|
164
|
+
if args.html_input is not None:
|
|
165
|
+
report.update(stage='binding', templateUsed='existing-canvas', fontPreset='existing', inputMode='html')
|
|
166
|
+
atomic_write(run / 'input.html', raw_bytes)
|
|
167
|
+
atomic_write(canvas, raw_bytes)
|
|
168
|
+
else:
|
|
169
|
+
atomic_write(run / 'input.json', raw_bytes)
|
|
170
|
+
supplied = contract.load_document(json.loads(raw_bytes))
|
|
171
|
+
if supplied['kind'] == 'variant':
|
|
172
|
+
raise ValueError('A Variant is not a Master; use its original Master to derive another resume')
|
|
173
|
+
if supplied['kind'] == 'draft' and not args.draft:
|
|
174
|
+
raise ValueError('Draft input cannot be delivered; review facts and provide a Master, or use --draft')
|
|
175
|
+
facts = copy.deepcopy(supplied['profile'])
|
|
176
|
+
for key in ('name', 'email', 'phone'):
|
|
177
|
+
if getattr(args, key) is not None:
|
|
178
|
+
facts['basics'][key] = getattr(args, key)
|
|
179
|
+
kind = 'draft' if args.draft else 'master'
|
|
180
|
+
master = contract.document(kind, facts, {'type': 'user-input', 'path': str(source) if source else None, 'inputMode': 'file' if source else 'stdin',
|
|
181
|
+
'inputSha256': hashlib.sha256(raw_bytes).hexdigest(),
|
|
182
|
+
'suppliedKind': supplied['kind'], 'suppliedSource': supplied['source']})
|
|
183
|
+
write_json(run / (kind + '.json'), master)
|
|
184
|
+
report['checks']['missingFields'] = master['missingFields']
|
|
185
|
+
report['warnings'].extend('Missing optional contact/position field: ' + v for v in master['missingFields'] if v != 'basics.name')
|
|
186
|
+
if not args.draft:
|
|
187
|
+
errors = contract.delivery_errors(master['profile'])
|
|
188
|
+
if errors:
|
|
189
|
+
raise ValueError('; '.join(errors))
|
|
190
|
+
profile = copy.deepcopy(master['profile'])
|
|
191
|
+
if args.role is not None:
|
|
192
|
+
profile['basics']['title'] = args.role
|
|
193
|
+
keywords = []
|
|
194
|
+
if args.jd:
|
|
195
|
+
report['stage'] = 'jd'
|
|
196
|
+
try:
|
|
197
|
+
jd_file = Path(args.jd)
|
|
198
|
+
jd = jd_file.read_text(encoding='utf-8') if jd_file.is_file() else args.jd
|
|
199
|
+
except OSError:
|
|
200
|
+
jd = args.jd
|
|
201
|
+
atomic_write(run / 'jd.txt', jd)
|
|
202
|
+
analyzer = load_module('jd_analyzer', 'evidence/analyze-jd.py')
|
|
203
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
204
|
+
result = analyzer.analyze_jd_text(jd)
|
|
205
|
+
write_json(run / 'jd-analysis.json', result)
|
|
206
|
+
keywords = result.get('detectedSkills', [])
|
|
207
|
+
report['stage'] = 'template'
|
|
208
|
+
template = args.template
|
|
209
|
+
if not template:
|
|
210
|
+
search = load_module('template_search', 'template/search-template.py')
|
|
211
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
212
|
+
results = search.search_templates(role=args.role or profile['basics'].get('title') or '',
|
|
213
|
+
keywords=','.join(keywords), target_pages=args.expected_pages, engine='hybrid')
|
|
214
|
+
if not results:
|
|
215
|
+
raise ValueError('No matching template available')
|
|
216
|
+
template = results[0].get('id') or results[0]['template']['id']
|
|
217
|
+
variant = contract.document('draft' if args.draft else 'variant', profile,
|
|
218
|
+
{'masterSha256': master['profileSha256'], 'masterPath': str(run / (kind + '.json')),
|
|
219
|
+
'template': template, 'targetRole': args.role, 'fontPreset': args.font_preset})
|
|
220
|
+
variant_path = run / ('draft-canvas-profile.json' if args.draft else 'variant.json')
|
|
221
|
+
write_json(variant_path, variant)
|
|
222
|
+
report['templateUsed'] = template
|
|
223
|
+
report['fontPreset'] = args.font_preset
|
|
224
|
+
report['stage'] = 'binding'
|
|
225
|
+
inst = load_module('instantiate_resume', 'template/instantiate-resume.py')
|
|
226
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
227
|
+
inst.instantiate_workspace(template, str(variant_path), ','.join(keywords), str(canvas), font_preset=args.font_preset)
|
|
196
228
|
if args.draft:
|
|
197
|
-
|
|
229
|
+
if args.html_output:
|
|
230
|
+
report['stage'] = 'publish'
|
|
231
|
+
publish_aliases([(canvas, Path(args.html_output).resolve())])
|
|
232
|
+
draft_outputs = {'htmlCanvas': str(canvas)}
|
|
233
|
+
if args.profile_json is not None:
|
|
234
|
+
draft_outputs['draft'] = str(run / 'draft.json')
|
|
235
|
+
report.update(status='DRAFT', stage='draft', outputs=draft_outputs)
|
|
236
|
+
if not retained:
|
|
237
|
+
report['outputs'] = {'htmlCanvas': str(Path(args.html_output).resolve())}
|
|
238
|
+
report['sha256'] = {'html': file_hash(canvas)}
|
|
198
239
|
exit_code = 0
|
|
199
240
|
else:
|
|
200
241
|
report['stage'] = 'qa'
|
|
@@ -244,11 +285,16 @@ def main():
|
|
|
244
285
|
if args.output:
|
|
245
286
|
pairs.append((pdf, Path(args.output).resolve()))
|
|
246
287
|
publish_aliases(pairs)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
288
|
+
outputs = {'htmlCanvas': str(canvas), 'pdfDelivery': str(pdf), 'qa': str(run / 'qa.json'),
|
|
289
|
+
'copies': [str(target) for _, target in pairs]}
|
|
290
|
+
hashes = {'html': file_hash(canvas), 'pdf': file_hash(pdf)}
|
|
291
|
+
if args.profile_json is not None:
|
|
292
|
+
outputs.update(master=str(run / 'master.json'), variant=str(variant_path))
|
|
293
|
+
hashes['master'] = master['profileSha256']
|
|
294
|
+
report.update(status='PASS', stage='complete', outputs=outputs, sha256=hashes)
|
|
295
|
+
if not retained:
|
|
296
|
+
report['outputs'] = {'htmlCanvas': str(Path(args.html_output).resolve()),
|
|
297
|
+
'pdfDelivery': str(Path(args.output).resolve())}
|
|
252
298
|
exit_code = 0
|
|
253
299
|
except Exception as error:
|
|
254
300
|
if report['status'] == 'RUNNING':
|
|
@@ -258,22 +304,50 @@ def main():
|
|
|
258
304
|
exit_code = 2 if report['status'] == 'UNVERIFIED' else 1
|
|
259
305
|
finally:
|
|
260
306
|
report['finishedAt'] = datetime.now(timezone.utc).isoformat()
|
|
261
|
-
if run:
|
|
307
|
+
if run and retained:
|
|
262
308
|
try:
|
|
263
309
|
write_json(run / 'manifest.json', report)
|
|
264
310
|
except OSError as error:
|
|
265
311
|
report.update(status='FAIL', outputs={})
|
|
266
312
|
report['errors'].append('Could not persist manifest: ' + str(error))
|
|
267
313
|
exit_code = 1
|
|
314
|
+
if temporary:
|
|
315
|
+
try:
|
|
316
|
+
temporary.cleanup()
|
|
317
|
+
except OSError as error:
|
|
318
|
+
report['warnings'].append('Temporary staging cleanup failed: ' + str(error))
|
|
268
319
|
emitted = report
|
|
269
|
-
if args.summary:
|
|
270
|
-
emitted = {key: report[key] for key in ('runId', 'status', 'stage', 'templateUsed', 'fontPreset', 'errors', 'warnings', 'outputs') if key in report}
|
|
320
|
+
if args.summary or not retained:
|
|
321
|
+
emitted = {key: report[key] for key in ('runId', 'status', 'stage', 'retention', 'templateUsed', 'fontPreset', 'errors', 'warnings', 'outputs', 'sha256') if key in report}
|
|
271
322
|
emitted['outputs'] = {}
|
|
272
323
|
if report['status'] in ('PASS', 'DRAFT'):
|
|
273
|
-
emitted['outputs']['html'] = str(Path(args.html_output).resolve()) if
|
|
324
|
+
emitted['outputs']['html'] = str(Path(args.html_output).resolve()) if args.html_output else report['outputs']['htmlCanvas']
|
|
274
325
|
if report['status'] == 'PASS':
|
|
275
326
|
emitted['outputs']['pdf'] = str(Path(args.output).resolve()) if args.output else report['outputs']['pdfDelivery']
|
|
276
|
-
emitted['manifest'] = str(run / 'manifest.json') if run else None
|
|
327
|
+
emitted['manifest'] = str(run / 'manifest.json') if run and retained else None
|
|
328
|
+
if report['status'] in ('FAIL', 'UNVERIFIED'):
|
|
329
|
+
failed_checks = report['checks'].get('qa', {}).get('checks', {})
|
|
330
|
+
details = {}
|
|
331
|
+
for key, check in failed_checks.items():
|
|
332
|
+
if check.get('status') != 'PASS':
|
|
333
|
+
details[key] = {'status': check.get('status')}
|
|
334
|
+
if key == 'dom':
|
|
335
|
+
if 'totalPagesFound' in check:
|
|
336
|
+
details[key]['totalPagesFound'] = check['totalPagesFound']
|
|
337
|
+
for field in ('issues', 'unsupported'):
|
|
338
|
+
if isinstance(check.get(field), list):
|
|
339
|
+
details[key][field + 'Count'] = len(check[field])
|
|
340
|
+
details[key][field] = check[field][:20]
|
|
341
|
+
if key == 'pdf':
|
|
342
|
+
for field in ('pageCount', 'pages'):
|
|
343
|
+
if field in check:
|
|
344
|
+
details[key][field] = check[field]
|
|
345
|
+
if key == 'text' and isinstance(check.get('pages'), list):
|
|
346
|
+
details[key]['pages'] = [
|
|
347
|
+
{field: page[field] for field in ('pageIndex', 'missingCodepoints', 'missingFragments', 'extractedRadicals') if field in page}
|
|
348
|
+
for page in check['pages'] if isinstance(page, dict)]
|
|
349
|
+
if details:
|
|
350
|
+
emitted['diagnostics'] = details
|
|
277
351
|
print(json.dumps(emitted, ensure_ascii=False, indent=None if args.quiet else 2))
|
|
278
352
|
return exit_code
|
|
279
353
|
|
|
@@ -74,13 +74,17 @@ function inspectDOM() {
|
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
if (e !== p) for (const r of e.getClientRects()) check(r, e.parentElement);
|
|
77
|
+
const tt = window.getComputedStyle(e).textTransform;
|
|
77
78
|
for (const node of e.childNodes) {
|
|
78
79
|
if (node.nodeType !== Node.TEXT_NODE || !node.textContent.trim()) continue;
|
|
79
80
|
const range = document.createRange();
|
|
80
81
|
range.selectNodeContents(node);
|
|
81
82
|
const rs = [...range.getClientRects()];
|
|
82
83
|
if (!rs.length) continue;
|
|
83
|
-
|
|
84
|
+
const transformed = tt === 'uppercase' ? node.textContent.toUpperCase() :
|
|
85
|
+
tt === 'lowercase' ? node.textContent.toLowerCase() :
|
|
86
|
+
node.textContent;
|
|
87
|
+
text.push(transformed);
|
|
84
88
|
for (const r of rs) check(r, e);
|
|
85
89
|
}
|
|
86
90
|
}
|
|
@@ -160,12 +160,14 @@ def render_profile_into_html(html_str, profile, keywords=None, *, css=""):
|
|
|
160
160
|
heading += "".join(field(item, k, "role-badge", "span") for k in ("role", "degree", "field"))
|
|
161
161
|
heading += dates(item)
|
|
162
162
|
content = f'<div class="item-header">{heading}</div>' if heading else ""
|
|
163
|
-
content += field(item, "location") + field(item, "summary")
|
|
163
|
+
content += field(item, "location", "item-location fact-text") + field(item, "summary", "item-summary fact-text")
|
|
164
164
|
gpa = item.get("gpa")
|
|
165
165
|
if gpa is not None and gpa != "":
|
|
166
166
|
content += element("p", "edu-detail", f"GPA: {gpa}")
|
|
167
|
-
content += field(item, "repoUrl") + field(item, "demoUrl")
|
|
168
|
-
|
|
167
|
+
content += field(item, "repoUrl", "project-url fact-text") + field(item, "demoUrl", "project-url fact-text")
|
|
168
|
+
tags = "".join(element("span", "tech-tag", t) for t in strings(item, "techStack"))
|
|
169
|
+
if tags:
|
|
170
|
+
content += f'<div class="tech-stack-tags">{tags}</div>'
|
|
169
171
|
content += "".join(element("p", "edu-detail", honor) for honor in strings(item, "honors"))
|
|
170
172
|
content += bullets(item)
|
|
171
173
|
if content:
|
package/skill.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/skill.json",
|
|
3
3
|
"name": "knowme-careerforge",
|
|
4
4
|
"displayName": "KnowMe CareerForge",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.8",
|
|
6
6
|
"description": "An agent-native skill for self-discovery, career positioning, and tailored resume engineering. Know yourself. Define your direction. Forge your opportunity.",
|
|
7
7
|
"author": "Miles <https://github.com/Max-Samson>",
|
|
8
8
|
"license": "MIT",
|
package/src/knowledge/index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.8",
|
|
3
3
|
"totalRoles": 9,
|
|
4
|
-
"totalTemplates":
|
|
4
|
+
"totalTemplates": 13,
|
|
5
5
|
"roles": [
|
|
6
6
|
{
|
|
7
7
|
"id": "ai-agent-engineer",
|
|
@@ -795,6 +795,63 @@
|
|
|
795
795
|
"--resume-space-section"
|
|
796
796
|
]
|
|
797
797
|
},
|
|
798
|
+
{
|
|
799
|
+
"id": "editorial",
|
|
800
|
+
"name": "Editorial · 经历叙事",
|
|
801
|
+
"version": "1.0.0",
|
|
802
|
+
"style": "single-column-editorial",
|
|
803
|
+
"roleCategory": "professional",
|
|
804
|
+
"description": "经历优先,左侧章节索引与右侧正文对齐;适合已有连续工作经历的候选人。",
|
|
805
|
+
"layout": {
|
|
806
|
+
"type": "single-column",
|
|
807
|
+
"targetPages": 1,
|
|
808
|
+
"maxPages": 1,
|
|
809
|
+
"density": "balanced"
|
|
810
|
+
},
|
|
811
|
+
"visualStyle": {
|
|
812
|
+
"tone": "editorial-readable",
|
|
813
|
+
"accentColor": "#254665",
|
|
814
|
+
"fontFamily": "System Sans-Serif"
|
|
815
|
+
},
|
|
816
|
+
"atsScoreTier": "tier-1-optimal",
|
|
817
|
+
"supportedRoles": [
|
|
818
|
+
"professional",
|
|
819
|
+
"consultant"
|
|
820
|
+
],
|
|
821
|
+
"languages": [
|
|
822
|
+
"zh-CN",
|
|
823
|
+
"en-US"
|
|
824
|
+
],
|
|
825
|
+
"customizableTokens": [
|
|
826
|
+
"--resume-font-size-body",
|
|
827
|
+
"--resume-line-height-body",
|
|
828
|
+
"--resume-space-section",
|
|
829
|
+
"--resume-space-item",
|
|
830
|
+
"--resume-space-bullet",
|
|
831
|
+
"--resume-color-accent"
|
|
832
|
+
],
|
|
833
|
+
"directory": "src/templates/editorial",
|
|
834
|
+
"detectedTokens": [
|
|
835
|
+
"--resume-color-accent",
|
|
836
|
+
"--resume-color-border",
|
|
837
|
+
"--resume-color-muted",
|
|
838
|
+
"--resume-color-primary",
|
|
839
|
+
"--resume-color-secondary",
|
|
840
|
+
"--resume-color-tag-bg",
|
|
841
|
+
"--resume-font-body",
|
|
842
|
+
"--resume-font-size-body",
|
|
843
|
+
"--resume-font-size-h2",
|
|
844
|
+
"--resume-font-size-meta",
|
|
845
|
+
"--resume-font-size-name",
|
|
846
|
+
"--resume-font-size-target",
|
|
847
|
+
"--resume-line-height-body",
|
|
848
|
+
"--resume-page-padding",
|
|
849
|
+
"--resume-space-bullet",
|
|
850
|
+
"--resume-space-header-bottom",
|
|
851
|
+
"--resume-space-item",
|
|
852
|
+
"--resume-space-section"
|
|
853
|
+
]
|
|
854
|
+
},
|
|
798
855
|
{
|
|
799
856
|
"id": "executive",
|
|
800
857
|
"name": "Executive Modern Split",
|
|
@@ -849,6 +906,7 @@
|
|
|
849
906
|
"--resume-font-size-body",
|
|
850
907
|
"--resume-font-size-meta",
|
|
851
908
|
"--resume-font-size-name",
|
|
909
|
+
"--resume-font-size-target",
|
|
852
910
|
"--resume-font-size-title",
|
|
853
911
|
"--resume-left-col-width",
|
|
854
912
|
"--resume-line-height-body",
|
|
@@ -857,6 +915,65 @@
|
|
|
857
915
|
"--resume-space-section"
|
|
858
916
|
]
|
|
859
917
|
},
|
|
918
|
+
{
|
|
919
|
+
"id": "graduate",
|
|
920
|
+
"name": "Graduate · 校园起点",
|
|
921
|
+
"version": "1.0.0",
|
|
922
|
+
"style": "single-column-education-first",
|
|
923
|
+
"roleCategory": "early-career",
|
|
924
|
+
"description": "教育与项目优先,宽松单栏;适合应届生、实习申请和较少工作经历。",
|
|
925
|
+
"layout": {
|
|
926
|
+
"type": "single-column",
|
|
927
|
+
"targetPages": 1,
|
|
928
|
+
"maxPages": 1,
|
|
929
|
+
"density": "balanced"
|
|
930
|
+
},
|
|
931
|
+
"visualStyle": {
|
|
932
|
+
"tone": "editorial-readable",
|
|
933
|
+
"accentColor": "#0f766e",
|
|
934
|
+
"fontFamily": "System Sans-Serif"
|
|
935
|
+
},
|
|
936
|
+
"atsScoreTier": "tier-1-optimal",
|
|
937
|
+
"supportedRoles": [
|
|
938
|
+
"graduate",
|
|
939
|
+
"intern",
|
|
940
|
+
"应届",
|
|
941
|
+
"实习"
|
|
942
|
+
],
|
|
943
|
+
"languages": [
|
|
944
|
+
"zh-CN",
|
|
945
|
+
"en-US"
|
|
946
|
+
],
|
|
947
|
+
"customizableTokens": [
|
|
948
|
+
"--resume-font-size-body",
|
|
949
|
+
"--resume-line-height-body",
|
|
950
|
+
"--resume-space-section",
|
|
951
|
+
"--resume-space-item",
|
|
952
|
+
"--resume-space-bullet",
|
|
953
|
+
"--resume-color-accent"
|
|
954
|
+
],
|
|
955
|
+
"directory": "src/templates/graduate",
|
|
956
|
+
"detectedTokens": [
|
|
957
|
+
"--resume-color-accent",
|
|
958
|
+
"--resume-color-border",
|
|
959
|
+
"--resume-color-muted",
|
|
960
|
+
"--resume-color-primary",
|
|
961
|
+
"--resume-color-secondary",
|
|
962
|
+
"--resume-color-tag-bg",
|
|
963
|
+
"--resume-font-body",
|
|
964
|
+
"--resume-font-size-body",
|
|
965
|
+
"--resume-font-size-h2",
|
|
966
|
+
"--resume-font-size-meta",
|
|
967
|
+
"--resume-font-size-name",
|
|
968
|
+
"--resume-font-size-target",
|
|
969
|
+
"--resume-line-height-body",
|
|
970
|
+
"--resume-page-padding",
|
|
971
|
+
"--resume-space-bullet",
|
|
972
|
+
"--resume-space-header-bottom",
|
|
973
|
+
"--resume-space-item",
|
|
974
|
+
"--resume-space-section"
|
|
975
|
+
]
|
|
976
|
+
},
|
|
860
977
|
{
|
|
861
978
|
"id": "international-flow",
|
|
862
979
|
"name": "International Text Flow",
|
|
@@ -1046,6 +1163,63 @@
|
|
|
1046
1163
|
"--resume-space-section"
|
|
1047
1164
|
]
|
|
1048
1165
|
},
|
|
1166
|
+
{
|
|
1167
|
+
"id": "project-focus",
|
|
1168
|
+
"name": "Project Focus · 项目实践",
|
|
1169
|
+
"version": "1.0.0",
|
|
1170
|
+
"style": "single-column-project-first",
|
|
1171
|
+
"roleCategory": "portfolio",
|
|
1172
|
+
"description": "项目优先,项目条目以细边线分隔;适合项目证据比工作履历更突出的候选人。",
|
|
1173
|
+
"layout": {
|
|
1174
|
+
"type": "single-column",
|
|
1175
|
+
"targetPages": 1,
|
|
1176
|
+
"maxPages": 1,
|
|
1177
|
+
"density": "balanced"
|
|
1178
|
+
},
|
|
1179
|
+
"visualStyle": {
|
|
1180
|
+
"tone": "editorial-readable",
|
|
1181
|
+
"accentColor": "#334155",
|
|
1182
|
+
"fontFamily": "System Sans-Serif"
|
|
1183
|
+
},
|
|
1184
|
+
"atsScoreTier": "tier-1-optimal",
|
|
1185
|
+
"supportedRoles": [
|
|
1186
|
+
"project-engineer",
|
|
1187
|
+
"portfolio"
|
|
1188
|
+
],
|
|
1189
|
+
"languages": [
|
|
1190
|
+
"zh-CN",
|
|
1191
|
+
"en-US"
|
|
1192
|
+
],
|
|
1193
|
+
"customizableTokens": [
|
|
1194
|
+
"--resume-font-size-body",
|
|
1195
|
+
"--resume-line-height-body",
|
|
1196
|
+
"--resume-space-section",
|
|
1197
|
+
"--resume-space-item",
|
|
1198
|
+
"--resume-space-bullet",
|
|
1199
|
+
"--resume-color-accent"
|
|
1200
|
+
],
|
|
1201
|
+
"directory": "src/templates/project-focus",
|
|
1202
|
+
"detectedTokens": [
|
|
1203
|
+
"--resume-color-accent",
|
|
1204
|
+
"--resume-color-border",
|
|
1205
|
+
"--resume-color-muted",
|
|
1206
|
+
"--resume-color-primary",
|
|
1207
|
+
"--resume-color-secondary",
|
|
1208
|
+
"--resume-color-tag-bg",
|
|
1209
|
+
"--resume-font-body",
|
|
1210
|
+
"--resume-font-size-body",
|
|
1211
|
+
"--resume-font-size-h2",
|
|
1212
|
+
"--resume-font-size-meta",
|
|
1213
|
+
"--resume-font-size-name",
|
|
1214
|
+
"--resume-font-size-target",
|
|
1215
|
+
"--resume-line-height-body",
|
|
1216
|
+
"--resume-page-padding",
|
|
1217
|
+
"--resume-space-bullet",
|
|
1218
|
+
"--resume-space-header-bottom",
|
|
1219
|
+
"--resume-space-item",
|
|
1220
|
+
"--resume-space-section"
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
1049
1223
|
{
|
|
1050
1224
|
"id": "startup-generalist",
|
|
1051
1225
|
"name": "Startup Generalist Cards",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"usage": "按用户实际动作选择动词;分类不代表职级或熟练度,不为了增强语气升级职责。中文优先具体表达,如实现、排查、协调;不用赋能、突破等词替代可描述的行动。",
|
|
2
3
|
"categories": {
|
|
3
4
|
"leadership": [
|
|
4
|
-
"主导", "统筹", "规划", "搭建", "
|
|
5
|
+
"主导", "统筹", "规划", "搭建", "协调", "组织", "重构", "制定",
|
|
5
6
|
"Spearheaded", "Led", "Architected", "Orchestrated", "Formulated", "Pioneered"
|
|
6
7
|
],
|
|
7
8
|
"engineering_architecture": [
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
"Designed", "Engineered", "Implemented", "Refactored", "Decoupled", "Migrated"
|
|
10
11
|
],
|
|
11
12
|
"optimization_performance": [
|
|
12
|
-
"优化", "缩减", "
|
|
13
|
+
"优化", "缩减", "分析", "降本", "提速", "消除", "治理", "排查",
|
|
13
14
|
"Optimized", "Reduced", "Accelerated", "Mitigated", "Eliminated", "Benchmarked"
|
|
14
15
|
],
|
|
15
16
|
"business_impact": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"model": "FAB (Feature - Advantage - Benefit)",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "可选写作辅助:组织已有动作、方法与结果;缺失部分省略,不要求量化,不补写经历。",
|
|
4
4
|
"components": {
|
|
5
5
|
"F": {
|
|
6
6
|
"name": "Feature (功能/动作)",
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"A": {
|
|
11
11
|
"name": "Advantage (优势/技术亮点)",
|
|
12
|
-
"meaning": "
|
|
12
|
+
"meaning": "用户实际采用的方法、技术或协作方式,不要求先进或创新",
|
|
13
13
|
"example": "基于 LangGraph 状态图与动态路由剪枝机制进行状态流转"
|
|
14
14
|
},
|
|
15
15
|
"B": {
|
|
16
16
|
"name": "Benefit (成效/量化价值)",
|
|
17
|
-
"meaning": "
|
|
17
|
+
"meaning": "用户提供的结果,可以是定性变化、交付事实或有依据的指标",
|
|
18
18
|
"example": "实现端到端任务成功率从 68% 提升至 94%,并缩减 Token 成本 35%"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
"standardBulletFormula": "[
|
|
21
|
+
"standardBulletFormula": "[实际职责动词] + [已知动作与对象] + [已知方法,可省略] + [已知结果,可省略]",
|
|
22
|
+
"usage": "示例为虚构写法,不是候选人事实。依据用户材料选择适合的结构,不把所有条目强制扩成完整 FAB。"
|
|
22
23
|
}
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"string",
|
|
61
61
|
"null"
|
|
62
62
|
],
|
|
63
|
-
"description": "
|
|
63
|
+
"description": "Optional concise synthesis of supported experience; length follows the language and content, not a fixed word count"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"additionalProperties": false
|