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/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 1f03e818..8185d674 100644 --- a/test/claims.test.ts +++ b/test/claims.test.ts @@ -322,3 +322,35 @@ describe("extractClaims — returns empty for missing file", () => { expect(claims).toEqual([]); }); }); + +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( + "labels.md", + "## Dependencies\n\n- **Frontend** — the UI\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(["Frontend", "Express", "@scope/pkg"]); + }); + + 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" + ); + const deps = extractClaims(path, "phrases.md").filter((c) => c.kind === "dependency"); + expect(deps).toEqual([]); + }); + + 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"); + }); +});