Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/portfolio-quality.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
4 changes: 3 additions & 1 deletion portfolio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 2 additions & 5 deletions portfolio/data/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
];

Expand Down
26 changes: 0 additions & 26 deletions portfolio/docs/alignment-notes.md

This file was deleted.

2 changes: 2 additions & 0 deletions portfolio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
81 changes: 81 additions & 0 deletions portfolio/scripts/publication-content-check.mjs
Original file line number Diff line number Diff line change
@@ -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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan preparation phrases across line boundaries

When a prohibited phrase is soft-wrapped across lines, as commonly happens in Markdown (for example, interview\npreparation), splitting the contents before applying rules means neither invocation sees the complete phrase; scanText therefore returns no findings and the repository publication check passes despite the rendered text containing the prohibited guidance. Scan the complete contents, or include adjacent-line overlap while retaining line-number calculation.

Useful? React with 👍 / 👎.

}
}
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();
22 changes: 22 additions & 0 deletions portfolio/tests/content-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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), []);
});
Loading