From dd9b6e6d3b62aed695136c7c06ce0a7453bdf1fc Mon Sep 17 00:00:00 2001 From: Christopher Bratkovics <36906152+cbratkovics@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:23:05 -0400 Subject: [PATCH] test(content): enforce repository-wide publication checks --- .github/workflows/portfolio-quality.yml | 4 +- portfolio/README.md | 4 +- portfolio/data/projects.ts | 7 +- portfolio/docs/alignment-notes.md | 26 ------ portfolio/package.json | 2 + .../scripts/publication-content-check.mjs | 81 +++++++++++++++++++ portfolio/tests/content-contract.test.mjs | 22 +++++ 7 files changed, 112 insertions(+), 34 deletions(-) delete mode 100644 portfolio/docs/alignment-notes.md create mode 100644 portfolio/scripts/publication-content-check.mjs diff --git a/.github/workflows/portfolio-quality.yml b/.github/workflows/portfolio-quality.yml index c04d254..385840f 100644 --- a/.github/workflows/portfolio-quality.yml +++ b/.github/workflows/portfolio-quality.yml @@ -1,10 +1,8 @@ name: Portfolio quality on: pull_request: - paths: ["portfolio/**", ".github/workflows/portfolio-quality.yml"] push: branches: [main] - paths: ["portfolio/**", ".github/workflows/portfolio-quality.yml"] permissions: contents: read jobs: @@ -21,8 +19,10 @@ jobs: cache: npm cache-dependency-path: portfolio/package-lock.json - run: npm ci + - run: npm run check:publication - run: npm run lint - run: npm run type-check - run: npm test - run: npm run build + - run: npm run check:publication:build - run: npm run test:e2e diff --git a/portfolio/README.md b/portfolio/README.md index ea7ab50..71b4052 100644 --- a/portfolio/README.md +++ b/portfolio/README.md @@ -24,14 +24,16 @@ npm start ## Quality checks ```bash +npm run check:publication npm run lint npm run type-check npm test npm run build +npm run check:publication:build npm run test:e2e ``` -`test:e2e` launches the previously built production server and checks initial HTML, fragment navigation contracts, contact policy, canonical metadata, discovery routes, and the generated social image. The GitHub Actions workflow runs the same sequence from a clean install. +The publication checks scan tracked first-party text across the repository and, after a build, generated deployable output. Diagnostics identify the path, line, and rule without printing matched content. `test:e2e` launches the previously built production server and checks initial HTML, fragment navigation contracts, contact policy, canonical metadata, discovery routes, and the generated social image. The GitHub Actions workflow runs the same sequence from a clean install. ## Structure diff --git a/portfolio/data/projects.ts b/portfolio/data/projects.ts index 280b4f1..f3b3c9a 100644 --- a/portfolio/data/projects.ts +++ b/portfolio/data/projects.ts @@ -140,13 +140,10 @@ export const projects: Project[] = [ id: "document-intelligence", title: "Document Intelligence | Local-First Retrieval Service", summary: "A local-first retrieval service with an authoritative SQLite manifest, staged ingestion/replacement/deletion, current-version hydration, scoped lexical and hybrid retrieval, and offline evaluation.", detail: "Document scope is applied to both retrieval branches, while unavailable-provider and excerpts-only outcomes are explicit. Dense retrieval, generation, and some reranking paths require configuration; citation validation checks references, not entailment, and sample tests are not a general quality benchmark.", - inspect: "Inspect the engineering case study, architecture, and lifecycle/retrieval tests; the repository documents a local walkthrough rather than claiming a verified hosted service.", + inspect: "Inspect the repository implementation and lifecycle/retrieval tests; the project documents a local walkthrough rather than claiming a verified hosted service.", tech: ["Python", "SQLite", "Hybrid retrieval", "FastAPI", "Offline evaluation"], githubUrl: "https://github.com/cbratkovics/document-intelligence-ai", - evidence: [ - { label: "Engineering case study", url: "https://github.com/cbratkovics/document-intelligence-ai/blob/main/docs/ENGINEERING_CASE_STUDY.md" }, - { label: "Architecture", url: "https://github.com/cbratkovics/document-intelligence-ai/blob/main/docs/ARCHITECTURE.md" } - ], featured: false + featured: false } ]; diff --git a/portfolio/docs/alignment-notes.md b/portfolio/docs/alignment-notes.md deleted file mode 100644 index 0a6661c..0000000 --- a/portfolio/docs/alignment-notes.md +++ /dev/null @@ -1,26 +0,0 @@ -# Portfolio alignment record — 2026-09-13 - -This sanitized matrix distinguishes facts supplied as the finalized professional record, inspected repository content, local verification, and work that still requires an authorized external action. No résumé, LinkedIn screenshot, contact detail, employer dataset, or private note is stored or published here. - -| Surface | Canonical fact or capability | Source / reference | Action in this repository | Verification performed | Unresolved external follow-up | -|---|---|---|---|---|---| -| Identity and metadata | Christopher J. Bratkovics; Data Scientist \| Analytics Engineer \| Applied AI; 7+ years in enterprise analytics | Finalized record supplied for this task | Aligned hero, site metadata, JSON-LD, social image, footer, and profile-only contact around canonical data | Source inspection, content contracts, production build/HTML and social-image route | After an authorized deployment, compare title, description, JSON-LD, and social preview on the active alias | -| Senior Data Analyst | April 2022–Present; seven ordered areas: five-platform foundation, migration, daily occupancy, regression/peers, churn/K-means, GenAI, cross-functional delivery | Finalized employment record supplied for this task | Restored seven distinct accomplishments; removed advertiser matching from this role; changed milestone `date` to `label` | Role-scoped deterministic contract and server-rendered HTML check | Reconcile LinkedIn wording manually if it differs; no profile was edited | -| BI Data Analyst | July 2019–April 2022; five ordered areas including entity resolution and production-release coordination | Finalized employment record supplied for this task | Added entity resolution and release coordination while keeping automation and dimensional modeling separate | Role-scoped count, order, and ownership contracts | Reconcile LinkedIn wording manually if it differs | -| Professional stories / highlights | Five stories with problem, contribution, decision, validation; precise occupancy ownership | Finalized record plus supported implementation detail supplied for this task | Centralized stories and highlights in the main data module; clarified completed SSP/integration work and collaborator-owned shared-capacity/charted components | Source inspection, content contracts, local production HTML | Employer code/data remain private and were not inspected or published | -| Fantasy Football | Per-position random forests, as-of features, separate GMM/PCA tiers; frozen-model historical 2025 OOS result: 5,914 player-weeks, 4.4909 vs 4.8046 PPR MAE (~6.5% computed reduction) | Artifact path; containing commit `df3e7e6dbae3446da15535176d1dee9f4f045433`; evaluation-code commit `8e22a47cfc99ba85861cccbe63732da42feb27ab`; blob `80fff5584a9ed50c6d895bd99232100c9b1bd77a` | Repaired immutable artifact URL and represented artifact/code commits separately | Values and computed-reduction contract; external retrieval was attempted but blocked by the environment's HTTP 403 tunnel | Recheck the immutable GitHub URL from an unrestricted network; verify demo separately after deployment | -| SQL Genius | Default: local reviewed templates, synthetic fixtures, browser SQLite, explicit execution, editable SQL, bounded preview, CSV; legacy Anthropic route optional | Maintained README, local generator, execution/policy files named in task; supplied inspected findings | Clarified default versus optional path and limits of read-only checks | Content contract; fresh upstream retrieval attempt blocked by HTTP 403 | Re-inspect upstream files from an unrestricted network; do not infer demo provider use | -| AI Chat | SSE, provider integration/failover, caching, bounded requests, structured errors, telemetry; scoped localhost evaluation used in-memory cache | `evals/results/latest.md` findings supplied for this task | Led with built capability; retained complete 10/10, 20/20, and 68-pair cache metrics plus dirty-tree/simulation/backend scope | Content contract; fresh upstream retrieval attempt blocked by HTTP 403 | Verify configured hosted backend independently; do not present checks as an SLA | -| NBA Stat Predictor | LightGBM batch pipeline, point-in-time features, artifact UI, replay, read-only grounded brief | `reports/metrics.json`, replay report, reconciliation and agent docs findings supplied for this task | Preserved cohort-qualified holdout result; added exact replay deltas and 0.05 tolerance to inspectable detail | Content contract; fresh upstream retrieval attempt blocked by HTTP 403 | Update stale LinkedIn alias/wording manually; verify hosted pages after deployment | -| Document Intelligence | Local-first manifest lifecycle, scoped lexical/hybrid retrieval, offline evaluation; dense/generation/reranking optional | Engineering case study and architecture findings supplied for this task | Preserved repository/local destination and configuration-dependent boundaries | Content contract; fresh upstream retrieval attempt blocked by HTTP 403 | No hosted demo is claimed; verify only if one is later intentionally published | -| Canonical host and privacy | Apex canonical; links-only GitHub/LinkedIn contact | Existing `SITE` configuration and task privacy policy | Reused configured identity/profile links in contact/footer; retained coherent canonical, robots, sitemap, Open Graph | Privacy scan, metadata routes, production HTML | Apex and `www` probes were attempted but blocked by HTTP 403; inspect redirects/DNS and live freshness after authorized deployment | - -## Authorized deployment verification checklist (not executed) - -1. Deploy only through the owner's normal authorized workflow, then record the intended Git commit and deployment identifier. -2. Resolve both `https://cbratkovics.dev` and `https://www.cbratkovics.dev`; record redirect chains and confirm the apex canonical remains correct before changing configuration. -3. Fetch uncached HTML from the active alias and confirm the distinctive strings `Production release coordination`, `Generative AI for executive communications`, and `df3e7e6dbae3446da15535176d1dee9f4f045433`. -4. Confirm all five repository destinations and the repaired immutable football artifact destination in delivered HTML. -5. Inspect canonical, Open Graph/Twitter metadata, JSON-LD, `robots.txt`, `sitemap.xml`, and the rendered social image. -6. Repeat viewport, keyboard, mobile-menu, details, skip-link, focus, overflow, and anchor-offset checks against the deployed build; do not treat a Vercel status alone as content verification. -7. Record CDN/crawler cache timestamps separately from application behavior if snapshots still differ. diff --git a/portfolio/package.json b/portfolio/package.json index cb9bdec..20d1152 100644 --- a/portfolio/package.json +++ b/portfolio/package.json @@ -8,6 +8,8 @@ "start": "next start", "lint": "eslint .", "type-check": "tsc --noEmit", + "check:publication": "node scripts/publication-content-check.mjs", + "check:publication:build": "node scripts/publication-content-check.mjs --generated", "test": "node --test tests/content-contract.test.mjs", "test:e2e": "node --test tests/e2e.test.mjs" }, diff --git a/portfolio/scripts/publication-content-check.mjs b/portfolio/scripts/publication-content-check.mjs new file mode 100644 index 0000000..b8b695e --- /dev/null +++ b/portfolio/scripts/publication-content-check.mjs @@ -0,0 +1,81 @@ +import { execFile } from "node:child_process"; +import { readdir, readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = path.resolve(scriptDirectory, "../.."); +const textExtensions = new Set([".css", ".html", ".js", ".json", ".jsx", ".md", ".mdx", ".mjs", ".svg", ".ts", ".tsx", ".txt", ".yaml", ".yml", ".xml"]); + +// Expressions are assembled so the publication checker can inspect its own source. +export const preparationRules = [ + { id: "interview-preparation", expression: new RegExp(`\\binterview(?:er|ing)?\\s+(?:${["prep", "preparation", "answers?", "narratives?", "talking points?"].join("|")})`, "i") }, + { id: "structured-response-coaching", expression: new RegExp(`\\b${["ST", "AR"].join("")}[- ](?:answers?|responses?|stories?)\\b`, "i") }, + { id: "profile-tailoring", expression: new RegExp(`\\b(?:tailor|rewrite|optimi[sz]e|align)\\s+(?:your\\s+|the\\s+)?(?:r[ée]sum[ée]|CV|LinkedIn)(?:\\s+(?:profile|bullets?|copy))?\\b`, "i") }, + { id: "interviewer-talking-points", expression: new RegExp(`\\b(?:say|tell|explain)\\s+(?:this\\s+)?to\\s+(?:an?\\s+|the\\s+)?interviewer\\b`, "i") }, + { id: "target-role-guidance", expression: new RegExp(`\\b(?:target(?:ing)?\\s+(?:a\\s+|the\\s+)?role|role[- ]targeting|${["career", "signal roadmap"].join("[- ]")})\\b`, "i") }, + { id: "recruiting-talking-points", expression: new RegExp(`\\b(?:recruiter|recruiting)\\s+(?:script|talking points?|pitch)\\b`, "i") }, +]; + +export const suspiciousDocumentName = new RegExp(`(?:^|/)(?:alignment[-_ ]notes|interview[-_ ](?:prep|notes)|career[-_ ]coaching|(?:r[ée]sum[ée]|linkedin)[-_ ](?:alignment|notes|guidance))(?:\\.[^/]*)?$`, "i"); + +export function scanText(contents) { + const findings = []; + for (const [index, line] of contents.split(/\r?\n/u).entries()) { + for (const rule of preparationRules) { + if (rule.expression.test(line)) findings.push({ line: index + 1, rule: rule.id }); + } + } + return findings; +} + +async function trackedFiles() { + const { stdout } = await execFileAsync("git", ["ls-files", "-z"], { cwd: repositoryRoot, encoding: "buffer" }); + return stdout.toString().split("\0").filter(Boolean); +} + +async function filesBelow(directory) { + try { + const entries = await readdir(directory, { withFileTypes: true }); + return (await Promise.all(entries.map(async (entry) => { + const item = path.join(directory, entry.name); + return entry.isDirectory() ? filesBelow(item) : [item]; + }))).flat(); + } catch (error) { + if (error.code === "ENOENT") return []; + throw error; + } +} + +async function scanFile(file, displayPath) { + if (!textExtensions.has(path.extname(file).toLowerCase())) return []; + if ((await stat(file)).size > 10_000_000) return []; + const contents = await readFile(file, "utf8"); + return scanText(contents).map(({ line, rule }) => ({ path: displayPath, line, rule })); +} + +export async function scanRepository({ generated = false } = {}) { + const findings = []; + for (const relative of await trackedFiles()) { + if (suspiciousDocumentName.test(relative)) findings.push({ path: relative, line: 1, rule: "suspicious-document-name" }); + findings.push(...await scanFile(path.join(repositoryRoot, relative), relative)); + } + if (generated) { + const outputRoot = path.join(repositoryRoot, "portfolio/.next"); + for (const file of await filesBelow(outputRoot)) findings.push(...await scanFile(file, path.relative(repositoryRoot, file))); + } + return findings; +} + +async function main() { + const findings = await scanRepository({ generated: process.argv.includes("--generated") }); + for (const finding of findings) console.error(`${finding.path}:${finding.line} [${finding.rule}]`); + if (findings.length) { + console.error(`Publication content check failed with ${findings.length} finding(s).`); + process.exitCode = 1; + } else console.log("Publication content check passed."); +} + +if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) await main(); diff --git a/portfolio/tests/content-contract.test.mjs b/portfolio/tests/content-contract.test.mjs index 8804fac..6fab80c 100644 --- a/portfolio/tests/content-contract.test.mjs +++ b/portfolio/tests/content-contract.test.mjs @@ -3,6 +3,7 @@ import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; +import { scanText, suspiciousDocumentName } from "../scripts/publication-content-check.mjs"; async function filesBelow(directory) { const entries = await readdir(directory, { withFileTypes: true }); @@ -97,3 +98,24 @@ test("publishable repository excludes private contact, career-status copy, and d assert.doesNotMatch(publicSource, /available for hire|open to opportunities|selectively exploring|seeking a new role/i); assert.equal(files.some((file) => /(?:resume|curriculum.vitae|linkedin.export).*\.(?:pdf|docx?|txt)$/i.test(file)), false); }); + +test("publication rules detect preparation guidance without reproducing matched content", () => { + const prohibitedExamples = [ + ["interview", " preparation"].join(""), + ["STAR", " answer"].join(""), + ["tailor your", " resume"].join(""), + ["recruiter", " talking points"].join(""), + ]; + for (const example of prohibitedExamples) assert.ok(scanText(example).length > 0); + assert.match(["alignment", "notes.md"].join("-"), suspiciousDocumentName); +}); + +test("publication rules preserve professional and technical language", () => { + const allowedExamples = [ + "A technical case study documents the design decision and measured result.", + "A reviewer validated the software terms against the source artifact.", + "Professional experience follows a clear problem, contribution, and validation narrative.", + "The parser extracts structured fields from uploaded documents.", + ]; + for (const example of allowedExamples) assert.deepEqual(scanText(example), []); +});