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.
Files changed (68) hide show
  1. package/AGENT.md +7 -6
  2. package/ARCHITECTURE.md +18 -15
  3. package/README.md +23 -15
  4. package/README.zh-CN.md +23 -15
  5. package/SKILL.md +26 -33
  6. package/agents/codex/knowme-careerforge.yaml +1 -1
  7. package/agents/opencode/skill.yaml +1 -1
  8. package/dist/cli/src/index.js +22 -14
  9. package/package.json +1 -1
  10. package/references/01-evidence-mining.md +80 -3
  11. package/references/02-career-goal.md +35 -15
  12. package/references/03-jd-analysis.md +10 -0
  13. package/references/04-template-selection.md +7 -2
  14. package/references/05-html-canvas-tokens.md +4 -2
  15. package/references/06-qa-and-rendering.md +39 -5
  16. package/references/07-artifact-contract.md +21 -7
  17. package/scripts/Agent.md +3 -3
  18. package/scripts/build/build-gallery.py +2 -1
  19. package/scripts/build/release.py +3 -2
  20. package/scripts/evidence/analyze-jd.py +49 -8
  21. package/scripts/evidence/extract-evidence.py +17 -391
  22. package/scripts/pipeline/forge.py +159 -85
  23. package/scripts/rendering/browser-engine.js +5 -1
  24. package/scripts/template/instantiate-resume.py +5 -3
  25. package/skill.json +1 -1
  26. package/src/knowledge/career/seniority-signals.json +1 -0
  27. package/src/knowledge/index.json +176 -2
  28. package/src/knowledge/resume/action-verbs.json +3 -2
  29. package/src/knowledge/resume/fab-model.json +5 -4
  30. package/src/knowledge/resume-schema.json +1 -1
  31. package/src/knowledge/templates.json +174 -0
  32. package/src/references/01-evidence-mining.md +80 -3
  33. package/src/references/02-career-goal.md +35 -15
  34. package/src/references/03-jd-analysis.md +10 -0
  35. package/src/references/04-template-selection.md +7 -2
  36. package/src/references/05-html-canvas-tokens.md +4 -2
  37. package/src/references/06-qa-and-rendering.md +39 -5
  38. package/src/references/07-artifact-contract.md +21 -7
  39. package/src/templates/academic-research/canvas.html +2 -1
  40. package/src/templates/classic/canvas.html +2 -1
  41. package/src/templates/classic/style.css +90 -28
  42. package/src/templates/common/base.css +19 -3
  43. package/src/templates/common/canvas-bindings.css +247 -12
  44. package/src/templates/compact-dense/style.css +5 -1
  45. package/src/templates/creative-tech/style.css +30 -1
  46. package/src/templates/data-analyst/style.css +31 -1
  47. package/src/templates/editorial/README.md +7 -0
  48. package/src/templates/editorial/canvas.html +22 -0
  49. package/src/templates/editorial/metadata.json +36 -0
  50. package/src/templates/editorial/sample-profile.json +86 -0
  51. package/src/templates/editorial/style.css +24 -0
  52. package/src/templates/executive/style.css +52 -18
  53. package/src/templates/graduate/README.md +7 -0
  54. package/src/templates/graduate/canvas.html +22 -0
  55. package/src/templates/graduate/metadata.json +38 -0
  56. package/src/templates/graduate/sample-profile.json +60 -0
  57. package/src/templates/graduate/style.css +24 -0
  58. package/src/templates/international-flow/canvas.html +2 -1
  59. package/src/templates/minimal/canvas.html +3 -2
  60. package/src/templates/minimal/style.css +6 -0
  61. package/src/templates/modern/style.css +43 -55
  62. package/src/templates/project-focus/README.md +7 -0
  63. package/src/templates/project-focus/canvas.html +22 -0
  64. package/src/templates/project-focus/metadata.json +36 -0
  65. package/src/templates/project-focus/sample-profile.json +93 -0
  66. package/src/templates/project-focus/style.css +24 -0
  67. package/src/templates/startup-generalist/canvas.html +2 -1
  68. package/src/templates/startup-generalist/style.css +15 -8
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Run an isolated user-input resume build with explicit artifact and QA states."""
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
- The run directory remains canonical. Lock files reject concurrent publishers
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.add_argument('--profile-json', required=True, help='User/Agent supplied facts or Draft/Master document; never a repository')
101
- parser.add_argument('--workspace', default='workspace/runs', help='Parent directory for unique run directories')
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='Optional verified PDF copy; default is the isolated run PDF')
110
- parser.add_argument('--html-output', help='Optional verified HTML copy; default is the isolated run canvas')
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 with manifest path; full diagnostics stay on disk')
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
- parent = Path(args.workspace).resolve()
122
- parent.mkdir(parents=True, exist_ok=True)
123
- run = parent / report['runId']
124
- run.mkdir(mode=0o700)
125
- report['runDirectory'] = str(run)
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
- source = Path(args.profile_json).resolve()
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
- if len(set([source] + aliases)) != len([source] + aliases):
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
- with contextlib.redirect_stdout(io.StringIO()):
195
- inst.instantiate_workspace(template, str(variant_path), ','.join(keywords), str(canvas), font_preset=args.font_preset)
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
- report.update(status='DRAFT', stage='draft', outputs={'draft': str(run / 'draft.json'), 'htmlCanvas': str(canvas)})
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
- report.update(status='PASS', stage='complete', outputs={
248
- 'master': str(run / 'master.json'), 'variant': str(variant_path),
249
- 'htmlCanvas': str(canvas), 'pdfDelivery': str(pdf), 'qa': str(run / 'qa.json'),
250
- 'copies': [str(target) for _, target in pairs]},
251
- sha256={'html': file_hash(canvas), 'pdf': file_hash(pdf), 'master': master['profileSha256']})
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 report['status'] == 'PASS' and args.html_output else report['outputs']['htmlCanvas']
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
- text.push(node.textContent);
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
- content += "".join(element("span", "tech-tag", t) for t in strings(item, "techStack"))
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.7",
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",
@@ -1,4 +1,5 @@
1
1
  {
2
+ "usage": "职级仅提供关注角度。动词依据实际职责选择,指标仅作为追问方向;不由年限或目标职级推断主导权、成果和熟练度。",
2
3
  "levels": {
3
4
  "junior": {
4
5
  "focus": "执行力、任务交付、代码质量与基础扎实度",
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.0.7",
2
+ "version": "0.0.8",
3
3
  "totalRoles": 9,
4
- "totalTemplates": 10,
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": "[强动词] + [Feature 核心系统/项目] + [Advantage 核心技术选型/创新解法] + [Benefit 量化业务/技术成效]"
21
+ "standardBulletFormula": "[实际职责动词] + [已知动作与对象] + [已知方法,可省略] + [已知结果,可省略]",
22
+ "usage": "示例为虚构写法,不是候选人事实。依据用户材料选择适合的结构,不把所有条目强制扩成完整 FAB。"
22
23
  }
@@ -60,7 +60,7 @@
60
60
  "string",
61
61
  "null"
62
62
  ],
63
- "description": "Core Value Proposition (15-30 words)"
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