From 4eaa089d0608693f6012e512c48cf1980c0b2888 Mon Sep 17 00:00:00 2001 From: abhinav-phi Date: Wed, 9 Sep 2026 05:50:05 +0530 Subject: [PATCH 1/2] feat(drift): filter non-package dependency claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every bold word in a stack/tech section became a dependency claim, so AWS, REST API, and Database Layer were checked against package.json and reported DEPENDENCY_MISSING — warnings the author cannot act on. Three structural filters now run after the existing name-shape check: all-caps words (acronyms), multi-word phrases (descriptive labels — scoped names like @mex/core keep their spacing exemption), and a curated architectural-label blocklist (frontend, middleware, auth...). Structural rules first, blocklist as the complement — real packages never collide with the labels because npm names like 'api' are far rarer in stack docs than the false positives they prevent. Resolves #4 --- src/drift/claims.ts | 27 +++++++++++++++++++++++++++ test/claims.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/drift/claims.ts b/src/drift/claims.ts index 8ad2312e..31171716 100644 --- a/src/drift/claims.ts +++ b/src/drift/claims.ts @@ -38,6 +38,24 @@ const DOTTED_KEY_WITH_SLASH = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+\/[A-Za-z0-9_ */ const PACKAGE_NAME = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/; +/** + * Architectural/descriptive terms that pass PACKAGE_NAME but are not + * installable packages — the blocklist complement to the structural + * heuristics above. One word per label; real packages never collide + * (npm names "api" or "database" are far rarer than the false positives + * these labels cause in stack docs). + */ +const ARCHITECTURAL_LABELS = new Set([ + "frontend", "backend", "fullstack", "full-stack", + "database", "database layer", "storage layer", "data layer", + "api", "api layer", "service layer", + "middleware", "infrastructure", "infra", "platform", + "auth", "authentication", "authorization", + "caching", "queue", "queues", "scheduler", "workers", + "server", "client", "monorepo", "tooling", "observability", + "testing", "deployment", "orchestration", "gateway", "firewall", +]); + /** Things that look like paths but are actually code snippets, URL routes, or other non-path content */ function isNotAPath(value: string): boolean { // URL routes: /voice/incoming, /api/users — start with / but have no file extension @@ -226,6 +244,15 @@ export function extractClaims(filePath: string, source: string): Claim[] { // a warning the author cannot act on. if (!PACKAGE_NAME.test(name)) return; + // Package-name heuristics (#4): a name that survives PACKAGE_NAME can + // still be a label no manifest could ever satisfy. Acronyms ("AWS", + // "REST") and multi-word descriptive phrases ("REST API", "Frontend + // Layer") are not packages; scoped names ("@mex/core") keep their slash. + const lower = name.toLowerCase(); + if (name === name.toUpperCase() && /[A-Z]/.test(name)) return; + if (/\s/.test(name) && !name.startsWith("@")) return; + if (ARCHITECTURAL_LABELS.has(lower)) return; + claims.push({ kind: "dependency", value: name, diff --git a/test/claims.test.ts b/test/claims.test.ts index 1f03e818..ddc1ec15 100644 --- a/test/claims.test.ts +++ b/test/claims.test.ts @@ -322,3 +322,41 @@ describe("extractClaims — returns empty for missing file", () => { expect(claims).toEqual([]); }); }); + +describe("extractClaims — non-package dependency filtering (#4)", () => { + it("drops all-caps acronyms from dependency sections", () => { + const path = writeFixture( + "acronyms.md", + "## Stack\n\n- **AWS** — cloud provider\n- **REST** — interface style\n- **JWT** — auth tokens\n" + ); + const deps = extractClaims(path, "acronyms.md").filter((c) => c.kind === "dependency"); + expect(deps).toEqual([]); + }); + + it("drops multi-word descriptive phrases", () => { + const path = writeFixture( + "phrases.md", + "## Tech Stack\n\n- **REST API** — external interface\n- **Database Layer** — persistence\n" + ); + const deps = extractClaims(path, "phrases.md").filter((c) => c.kind === "dependency"); + expect(deps).toEqual([]); + }); + + it("drops common architectural labels but keeps real packages", () => { + const path = writeFixture( + "labels.md", + "## Dependencies\n\n- **Frontend** — the UI\n- **Middleware** — request pipeline\n- **Express** — web framework\n- **@scope/pkg** — internal\n" + ); + const deps = extractClaims(path, "labels.md").filter((c) => c.kind === "dependency"); + expect(deps.map((d) => d.value)).toEqual(["Express", "@scope/pkg"]); + }); + + it("keeps mixed-case package names with digits", () => { + const path = writeFixture( + "packages.md", + "## Stack\n\n- **YouTube.js** — client\n- **pino-http** — logging\n" + ); + const deps = extractClaims(path, "packages.md").filter((c) => c.kind === "dependency"); + expect(deps.map((d) => d.value)).toContain("pino-http"); + }); +}); From 7a833643e19ac40b33accf29ab9436c418a5975a Mon Sep 17 00:00:00 2001 From: Yashasvi Date: Fri, 18 Sep 2026 16:59:30 +0530 Subject: [PATCH 2/2] fix(drift): filter non-package claims where the manifest is known Two of the three extractor rules did not hold up. The multi-word rule could not fire: PACKAGE_NAME rejects whitespace and runs three lines above it, so "REST API" and "Database Layer" were already dropped before this branch. Its test passed on main too, and four entries in the label list -- "database layer", "api layer", "service layer", "storage layer" -- could never be looked up for the same reason. The acronym rule read as "contains no lowercase letter", which is wider than an acronym. It dropped PINO-HTTP, GRAPHQL-WS, YOUTUBE.JS and @SCOPE/PKG -- the last contradicting the rule's own comment about scoped names -- and, because it deleted the claim in the extractor, a package written in capitals stopped being checked at all: with cors and ajv gone from the manifest, main reported both and this branch reported neither. The filtering now runs in the dependency checker, beside KNOWN_RUNTIMES, which is the layer that already suppresses this kind of name and the only one that knows what the project declares. A label that is also a real package (`server`, `client`, `queue`, `middleware`) stays checked when the manifest carries it, the set of dependency claims that `mex check` uses to keep a package name from being reported as a broken path stays intact, and a version claim on a capitalized name is still compared. The acronym pattern is narrowed to one unseparated word, so capitalized package spellings survive. What remains, deliberately: a name pattern cannot separate "CORS the acronym" from "cors was removed from the manifest", so drift on a package short enough to be written in capitals goes unreported. That trade is the point of the issue; it is written down in the code and the CHANGELOG rather than left to be discovered. Measured on a stack section mixing labels, acronyms and real packages: eight warnings before, one after -- the only claim naming a package the manifest does not carry. Tests moved to the checker and extended with the capitalized-package and declared-label cases; each fails without its fix, including against the original rule. --- CHANGELOG.md | 1 + src/drift/checkers/dependency.ts | 45 ++++++++++++++++++++++++ src/drift/claims.ts | 27 -------------- test/checkers.test.ts | 60 ++++++++++++++++++++++++++++++++ test/claims.test.ts | 26 ++++++-------- 5 files changed, 116 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 794fbac8..c337328b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ## [0.8.2] - Unreleased ### Added +- A stack section's architectural vocabulary no longer produces dependency warnings. `mex check` skips a claim that names a part of a system rather than a package — labels such as **Frontend**, **Middleware** or **Observability**, and concept acronyms such as **SPA**, **CRUD**, **MVC** or **SSR** — alongside the runtimes and platforms it already skipped. The rule is applied in the dependency checker, where the manifest is known, so a project that genuinely declares a package by one of those names keeps it checked, and a capitalized package spelling carrying separators (`PINO-HTTP`, `GRAPHQL-WS`, `@SCOPE/PKG`) is unaffected. The accepted cost: a package whose name is short enough to be written in capitals (`cors`, `ajv`) is no longer reported once it disappears from the manifest, because nothing in the name separates that from an acronym; version claims on such a name are still compared (#4). - Dependency claims are now checked against `pyproject.toml`. A bounded line scan reads `[project] dependencies`, the per-extra arrays in `[project.optional-dependencies]`, PEP 735 `[dependency-groups]`, and `[tool.poetry.dependencies]` together with its named groups — arrays written one item per line included, since that is what Python packaging tools emit. PEP 508 specifiers give the package name with the constraint kept as version evidence, environment markers no longer truncate the rest of the array, a poetry inline table contributes its `version` constraint rather than the whole table, and a project listed in its own `all` extra is not read as a dependency of itself. A Python claim also matches its PEP 503 equivalent, so prose writing the import spelling (`sentence_transformers`) no longer contradicts a manifest carrying the distribution name (`sentence-transformers`); npm names stay exact, where `lodash.debounce` and `lodash-debounce` are different packages. Python projects previously had every documented package reported `DEPENDENCY_MISSING`, or — with no `package.json` anywhere — no dependency checking at all (#3). - Setup completion guide with fresh-session verification, optional version-pinned global installation, and optional embedded email/name contact submission through Web3Forms. Only submitted/skipped contact markers are saved per computer; contact details stay out of project files and telemetry. - A bounded Next.js App Router resolver turning `app/**/route.ts|js` modules (including `src/app` roots) into route nodes: one per exported HTTP handler (`GET` through `HEAD`), with the URL path derived from the route file's directory, dynamic segments such as `[id]` and catch-alls preserved verbatim, and route groups `(marketing)` excluded the way Next resolves them. Same-file handlers resolve only when unambiguous; Pages Router, layouts, and pages stay out of scope (#95). diff --git a/src/drift/checkers/dependency.ts b/src/drift/checkers/dependency.ts index 2d077e0d..64851536 100644 --- a/src/drift/checkers/dependency.ts +++ b/src/drift/checkers/dependency.ts @@ -37,6 +37,47 @@ const KNOWN_RUNTIMES = new Set([ "linux", "macos", "windows", "wasm", "webassembly", ]); +/** + * Architectural and descriptive labels that name a part of a system rather + * than something installable (#4). The complement to the acronym pattern + * below: a single word like "Frontend" has the shape of a package name, so + * only a list can catch it. + * + * Several of these — `server`, `client`, `queue`, `middleware`, `platform` — + * are also real npm packages. That is safe here and would not be safe in the + * claim extractor: a project that genuinely depends on one declares it in a + * manifest, the lookup below finds it, and nothing is reported either way. + * The list only suppresses a warning about a package nothing declares. + */ +const NON_PACKAGE_LABELS = new Set([ + "frontend", "backend", "fullstack", "full-stack", + "database", "storage", "persistence", + "middleware", "infrastructure", "infra", "platform", + "authentication", "authorization", + "caching", "queue", "queues", "scheduler", "workers", + "server", "client", "monorepo", "tooling", "observability", + "testing", "deployment", "orchestration", "gateway", "firewall", +]); + +/** + * An acronym names an architectural concept — `SPA`, `CRUD`, `MVC`, `SSR`, + * `DDD` — not a package, so a claim written this way can never be satisfied + * by a manifest. + * + * Deliberately narrow: one unseparated word. A capitalized package spelling + * keeps its separators (`PINO-HTTP`, `GRAPHQL-WS`, `YOUTUBE.JS`, `@SCOPE/PKG`) + * and is still checked. + * + * The residual cost, accepted: a package with a name short enough to be + * written in capitals (`cors`, `ajv`, `d3`) stops being reported once it is + * dropped from the manifest, because nothing in the name separates that from + * an acronym. Version claims are unaffected — `**D3 7.0**` is still compared + * against the manifest below. + */ +function isConceptAcronym(value: string): boolean { + return /^[A-Z][A-Z0-9]*$/.test(value); +} + /** Check that claimed dependencies exist in manifests */ export function checkDependencies( claims: Claim[], @@ -59,6 +100,10 @@ export function checkDependencies( // Skip known runtimes/platforms — they won't be in package.json if (KNOWN_RUNTIMES.has(name)) continue; + // Skip what a stack section calls a part of the system rather than a + // package: "Frontend", "Observability", "SPA" (#4). + if (NON_PACKAGE_LABELS.has(name) || isConceptAcronym(claim.value)) continue; + // Fuzzy match: "React" → "react", "Express" → "express" const found = findDependency(deps, name); if (!found) { diff --git a/src/drift/claims.ts b/src/drift/claims.ts index 31171716..8ad2312e 100644 --- a/src/drift/claims.ts +++ b/src/drift/claims.ts @@ -38,24 +38,6 @@ const DOTTED_KEY_WITH_SLASH = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+\/[A-Za-z0-9_ */ const PACKAGE_NAME = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/; -/** - * Architectural/descriptive terms that pass PACKAGE_NAME but are not - * installable packages — the blocklist complement to the structural - * heuristics above. One word per label; real packages never collide - * (npm names "api" or "database" are far rarer than the false positives - * these labels cause in stack docs). - */ -const ARCHITECTURAL_LABELS = new Set([ - "frontend", "backend", "fullstack", "full-stack", - "database", "database layer", "storage layer", "data layer", - "api", "api layer", "service layer", - "middleware", "infrastructure", "infra", "platform", - "auth", "authentication", "authorization", - "caching", "queue", "queues", "scheduler", "workers", - "server", "client", "monorepo", "tooling", "observability", - "testing", "deployment", "orchestration", "gateway", "firewall", -]); - /** Things that look like paths but are actually code snippets, URL routes, or other non-path content */ function isNotAPath(value: string): boolean { // URL routes: /voice/incoming, /api/users — start with / but have no file extension @@ -244,15 +226,6 @@ export function extractClaims(filePath: string, source: string): Claim[] { // a warning the author cannot act on. if (!PACKAGE_NAME.test(name)) return; - // Package-name heuristics (#4): a name that survives PACKAGE_NAME can - // still be a label no manifest could ever satisfy. Acronyms ("AWS", - // "REST") and multi-word descriptive phrases ("REST API", "Frontend - // Layer") are not packages; scoped names ("@mex/core") keep their slash. - const lower = name.toLowerCase(); - if (name === name.toUpperCase() && /[A-Z]/.test(name)) return; - if (/\s/.test(name) && !name.startsWith("@")) return; - if (ARCHITECTURAL_LABELS.has(lower)) return; - claims.push({ kind: "dependency", value: name, diff --git a/test/checkers.test.ts b/test/checkers.test.ts index 1cf28c46..6368c102 100644 --- a/test/checkers.test.ts +++ b/test/checkers.test.ts @@ -610,6 +610,66 @@ describe("checkDependencies", () => { expect(issues).toHaveLength(1); expect(issues[0].code).toBe("DEPENDENCY_MISSING"); }); + + it("does not report architectural labels or concept acronyms (#4)", () => { + writeFileSync( + join(tmpDir, "package.json"), + JSON.stringify({ dependencies: { express: "^4.18.0" } }) + ); + const issues = checkDependencies([ + claim({ kind: "dependency", value: "Frontend" }), + claim({ kind: "dependency", value: "Middleware" }), + claim({ kind: "dependency", value: "Observability" }), + claim({ kind: "dependency", value: "SPA" }), + claim({ kind: "dependency", value: "CRUD" }), + claim({ kind: "dependency", value: "SSR" }), + claim({ kind: "dependency", value: "Express" }), + claim({ kind: "dependency", value: "fastify" }), + ], tmpDir); + expect(issues.map((i) => i.claim.value)).toEqual(["fastify"]); + }); + + it("keeps checking capitalized package spellings that carry separators (#4)", () => { + writeFileSync( + join(tmpDir, "package.json"), + JSON.stringify({ dependencies: { express: "^4.18.0" } }) + ); + const issues = checkDependencies([ + claim({ kind: "dependency", value: "PINO-HTTP" }), + claim({ kind: "dependency", value: "GRAPHQL-WS" }), + claim({ kind: "dependency", value: "YOUTUBE.JS" }), + claim({ kind: "dependency", value: "@SCOPE/PKG" }), + ], tmpDir); + expect(issues.map((i) => i.claim.value)).toEqual([ + "PINO-HTTP", "GRAPHQL-WS", "YOUTUBE.JS", "@SCOPE/PKG", + ]); + }); + + it("a label that is a declared dependency is still verified (#4)", () => { + writeFileSync( + join(tmpDir, "package.json"), + JSON.stringify({ dependencies: { middleware: "^1.0.0" } }) + ); + const issues = checkDependencies([ + claim({ kind: "dependency", value: "middleware" }), + claim({ kind: "version", value: "middleware 2.0" }), + ], tmpDir); + expect(issues).toHaveLength(1); + expect(issues[0].code).toBe("VERSION_MISMATCH"); + }); + + it("a version claim on a capitalized package is still compared (#4)", () => { + writeFileSync( + join(tmpDir, "package.json"), + JSON.stringify({ dependencies: { d3: "^6.2.0" } }) + ); + const issues = checkDependencies([ + claim({ kind: "dependency", value: "D3" }), + claim({ kind: "version", value: "D3 7.0" }), + ], tmpDir); + expect(issues).toHaveLength(1); + expect(issues[0].code).toBe("VERSION_MISMATCH"); + }); }); // ── Cross-file Checker ── diff --git a/test/claims.test.ts b/test/claims.test.ts index ddc1ec15..8185d674 100644 --- a/test/claims.test.ts +++ b/test/claims.test.ts @@ -323,17 +323,20 @@ describe("extractClaims — returns empty for missing file", () => { }); }); -describe("extractClaims — non-package dependency filtering (#4)", () => { - it("drops all-caps acronyms from dependency sections", () => { +describe("extractClaims — non-package names (#4)", () => { + it("keeps a bold name that has package shape, whatever it denotes", () => { + // Filtering belongs in the dependency checker, where the manifest says + // whether a name is a package this project actually declares. The + // extractor reports what the document claims. const path = writeFixture( - "acronyms.md", - "## Stack\n\n- **AWS** — cloud provider\n- **REST** — interface style\n- **JWT** — auth tokens\n" + "labels.md", + "## Dependencies\n\n- **Frontend** — the UI\n- **Express** — web framework\n- **@scope/pkg** — internal\n" ); - const deps = extractClaims(path, "acronyms.md").filter((c) => c.kind === "dependency"); - expect(deps).toEqual([]); + const deps = extractClaims(path, "labels.md").filter((c) => c.kind === "dependency"); + expect(deps.map((d) => d.value)).toEqual(["Frontend", "Express", "@scope/pkg"]); }); - it("drops multi-word descriptive phrases", () => { + it("drops multi-word phrases, which have no package shape", () => { const path = writeFixture( "phrases.md", "## Tech Stack\n\n- **REST API** — external interface\n- **Database Layer** — persistence\n" @@ -342,15 +345,6 @@ describe("extractClaims — non-package dependency filtering (#4)", () => { expect(deps).toEqual([]); }); - it("drops common architectural labels but keeps real packages", () => { - const path = writeFixture( - "labels.md", - "## Dependencies\n\n- **Frontend** — the UI\n- **Middleware** — request pipeline\n- **Express** — web framework\n- **@scope/pkg** — internal\n" - ); - const deps = extractClaims(path, "labels.md").filter((c) => c.kind === "dependency"); - expect(deps.map((d) => d.value)).toEqual(["Express", "@scope/pkg"]); - }); - it("keeps mixed-case package names with digits", () => { const path = writeFixture( "packages.md",