My Gatsby Deployment pipeline / build-and-deploy (push) Successful in 1m4s
Replace the single Gatsby page with a dark, terminal-style agent that answers questions about Dennis's work from one curated knowledge base. There is no model and no network call, so answers cannot be invented. A plain "Classic view" and an optional light theme sit one click away. Site - Terminal UI (React island): typed answers, tool-call lines, slash commands, autocomplete, history, deep links, a short /oncall game - Content focused on what recruiters scan for; personal and employer-internal details are left out - No profile photo anywhere, including the new link-preview card and icons - Corrected facts from the old page (dates, titles, tenses, skill tiers) Tooling - Migrate Gatsby 5 to Astro 7 (Node 22.23+); builds in about 1 second - Self-host fonts, so visitors make no request to third parties - Link-preview card, favicon set, web manifest and a service-worker clean-up for returning visitors - Deploy workflow builds with npm ci on Node 22 and uploads dist/ - Track package-lock.json Quality - Tests for routing and for markup injection from visitor input - A content guard scans every reachable answer; the list of things that must never be published lives in a git-ignored local file Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
// Guards the public wording against claims the resume kit rules out. Every answer the
|
|
// terminal can reach by clicking is generated and scanned, so a new sentence in
|
|
// profile.mjs cannot slip a banned claim onto the site.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import { answerToText, COMMANDS, INTRO, respond } from '../src/agent/engine.mjs'
|
|
import * as profile from '../src/data/profile.mjs'
|
|
import { privatePatterns } from './private-banned.mjs'
|
|
|
|
// Generic rules. The specifics that must stay hidden live in the git-ignored
|
|
// test/banned.private.json, so this file never spells them out.
|
|
const BANNED = [
|
|
[
|
|
/typescript|terraform|azure|\bgcp\b|snowflake/i,
|
|
'skill that is not on the record',
|
|
],
|
|
[/langchain|langgraph|llamaindex|llama-index/i, 'agent framework never used'],
|
|
[/fine-?tun/i, 'model fine-tuning'],
|
|
[/petabyte|\bPB-scale/i, 'scale claim'],
|
|
[
|
|
/own the (aws )?data platform|built the data mesh|data mesh/i,
|
|
'org-scale ownership',
|
|
],
|
|
[
|
|
/customer-embedded|formal model evaluation|agent orchestration|production llm/i,
|
|
'forbidden pattern',
|
|
],
|
|
[
|
|
/led the migration|sole technical lead|owned the full migration|managed a team|led engineers/i,
|
|
'ownership overreach',
|
|
],
|
|
[/24\/7 on-?call|on-?call 24\/7/i, 'on-call hours are not on the record'],
|
|
[/\bmttr\b|\buptime\b.*\d+\s?%/i, 'operational metric'],
|
|
[/\+\d{2}[\s\d().-]{8,}/, 'phone number'],
|
|
[/\bgrade\b/i, 'degree grades stay off the page'],
|
|
[
|
|
/permit|sponsorship|authori[sz]ed to work|work authori[sz]ation/i,
|
|
'work authorization stays off the page',
|
|
],
|
|
...privatePatterns,
|
|
]
|
|
|
|
const scan = (label, value) => {
|
|
for (const [pattern, why] of BANNED) {
|
|
assert.doesNotMatch(value, pattern, `${label}: ${why}`)
|
|
}
|
|
}
|
|
|
|
test('static content avoids banned claims', () => {
|
|
const { NON_CLAIMS, ...content } = profile
|
|
scan('profile.mjs', JSON.stringify(content))
|
|
})
|
|
|
|
test('every answer reachable from the intro avoids banned claims and dead ends', () => {
|
|
const seen = new Set()
|
|
const queue = [
|
|
...INTRO.followups.map((f) => [f.cmd, null]),
|
|
...COMMANDS.filter((c) => !['clear', 'theme'].includes(c.name)).map((c) => [
|
|
`/${c.name}`,
|
|
null,
|
|
]),
|
|
]
|
|
let visited = 0
|
|
while (queue.length) {
|
|
const [cmd, scene] = queue.shift()
|
|
const key = `${cmd}|${JSON.stringify(scene)}`
|
|
if (seen.has(key)) continue
|
|
seen.add(key)
|
|
const out = respond(cmd, scene)
|
|
if (!out.answer) continue
|
|
visited += 1
|
|
const body = answerToText(out.answer)
|
|
scan(cmd, body)
|
|
assert.doesNotMatch(
|
|
body,
|
|
/Unknown command|No (role|project) matches|outside what I can answer/,
|
|
`${cmd} led to a dead end`
|
|
)
|
|
for (const f of out.answer.followups) queue.push([f.cmd, out.scene])
|
|
}
|
|
assert.ok(visited > 30, `crawled ${visited} answers`)
|
|
})
|
|
|
|
test('every role and project opens', () => {
|
|
for (const role of profile.ROLES) {
|
|
assert.match(
|
|
answerToText(respond(`/experience ${role.id}`).answer),
|
|
new RegExp(role.company)
|
|
)
|
|
}
|
|
for (const project of profile.PROJECTS) {
|
|
const body = answerToText(respond(`/projects ${project.id}`).answer)
|
|
assert.ok(body.includes(project.name), project.id)
|
|
scan(project.id, body)
|
|
}
|
|
})
|
|
|
|
test('every skill can be asked about by name', () => {
|
|
for (const skill of profile.SKILLS) {
|
|
const body = answerToText(
|
|
respond(`Does he know ${skill.aliases[0]}?`).answer
|
|
)
|
|
assert.ok(
|
|
body.includes(skill.note),
|
|
`${skill.name} should answer with its own note`
|
|
)
|
|
}
|
|
})
|
|
|
|
test('job titles and dates match the record', () => {
|
|
const byId = Object.fromEntries(profile.ROLES.map((r) => [r.id, r]))
|
|
assert.equal(byId.bosch.period, '2020 to 2022')
|
|
assert.equal(byId.fraunhofer.period, '2018 to 2019')
|
|
assert.equal(byId.vizrt.title, 'Test Automation / DevOps Engineer')
|
|
assert.equal(byId.generali.title, 'IT Consultant')
|
|
assert.equal(
|
|
byId.swisscom.titleHistory,
|
|
'Senior from Oct 2023, Staff since Apr 2025'
|
|
)
|
|
})
|
|
|
|
test('the not-in-stack list holds only mainstream technologies', () => {
|
|
const allowed = new Set([
|
|
'TypeScript',
|
|
'Terraform',
|
|
'Azure',
|
|
'GCP',
|
|
'Snowflake',
|
|
'LangChain',
|
|
'LangGraph',
|
|
'LlamaIndex',
|
|
'Model fine-tuning',
|
|
])
|
|
for (const [label] of profile.NON_CLAIMS) {
|
|
assert.ok(
|
|
allowed.has(label),
|
|
`${label} should not be published in the not-in-stack list`
|
|
)
|
|
}
|
|
})
|