feat(project-facts): architecture-pattern signals - #7
Merged
Conversation
Comment on lines
+397
to
+453
| function getArchitectureSignals(rootDir) { | ||
| const signals = []; | ||
| const pkg = readJsonSafe(path.join(rootDir, "package.json"), {}); | ||
|
|
||
| if (pkg.bin && (typeof pkg.bin === "string" || (typeof pkg.bin === "object" && Object.keys(pkg.bin).length > 0))) { | ||
| const binNames = typeof pkg.bin === "string" ? [path.basename(rootDir)] : Object.keys(pkg.bin); | ||
| signals.push({ name: "CLI tool", evidence: "package.json \"bin\": " + binNames.join(", ") }); | ||
| } | ||
|
|
||
| if (pkg.main || pkg.exports) { | ||
| signals.push({ name: "Publishable library", evidence: "package.json has a \"" + (pkg.exports ? "exports" : "main") + "\" field" }); | ||
| } | ||
|
|
||
| const workspacePackages = getWorkspacePackages(rootDir); | ||
| if (workspacePackages.length > 0) { | ||
| signals.push({ | ||
| name: "Monorepo (npm workspaces)", | ||
| evidence: workspacePackages.length + " workspace package(s): " + workspacePackages.map(function (p) { return p.name; }).join(", "), | ||
| }); | ||
| } | ||
|
|
||
| const dependencyFrameworks = getFrameworkSignals(rootDir).filter(function (s) { return s.confidence === "dependency"; }); | ||
| const dependencyNames = dependencyFrameworks.map(function (s) { return s.name; }); | ||
|
|
||
| const backendFrameworks = dependencyNames.filter(function (n) { return n === "Express" || n === "NestJS"; }); | ||
| if (backendFrameworks.length > 0) { | ||
| signals.push({ name: "Backend/API service", evidence: "framework dependency: " + backendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const frontendFrameworks = dependencyNames.filter(function (n) { return ["React", "Next.js", "Angular", "Vue"].indexOf(n) !== -1; }); | ||
| if (frontendFrameworks.length > 0) { | ||
| signals.push({ name: "Frontend application", evidence: "framework dependency: " + frontendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const dirNames = collectDirectoryNamesFromStructure(getProjectStructure(rootDir)); | ||
|
|
||
| const matchedMvc = MVC_DIR_NAMES.filter(function (n) { return dirNames.has(n); }); | ||
| if (matchedMvc.length >= 2) { | ||
| signals.push({ name: "MVC-influenced layout", evidence: "directories present: " + matchedMvc.join(", ") }); | ||
| } | ||
|
|
||
| // One signal, not one per group: the rule is "group A matches OR | ||
| // group B matches", not two independent observations. If both | ||
| // groups happen to match, report both as evidence on the single | ||
| // signal rather than emitting a duplicate-named entry. | ||
| const matchedLayeredGroups = LAYERED_DIR_NAME_GROUPS | ||
| .map(function (group) { return group.filter(function (n) { return dirNames.has(n); }); }) | ||
| .filter(function (matched) { return matched.length >= 2; }); | ||
| if (matchedLayeredGroups.length > 0) { | ||
| signals.push({ | ||
| name: "Layered/service-oriented layout", | ||
| evidence: "directories present: " + matchedLayeredGroups.map(function (g) { return g.join(", "); }).join(" / "), | ||
| }); | ||
| } | ||
|
|
||
| return signals; | ||
| } |
Comment on lines
+397
to
+453
| function getArchitectureSignals(rootDir) { | ||
| const signals = []; | ||
| const pkg = readJsonSafe(path.join(rootDir, "package.json"), {}); | ||
|
|
||
| if (pkg.bin && (typeof pkg.bin === "string" || (typeof pkg.bin === "object" && Object.keys(pkg.bin).length > 0))) { | ||
| const binNames = typeof pkg.bin === "string" ? [path.basename(rootDir)] : Object.keys(pkg.bin); | ||
| signals.push({ name: "CLI tool", evidence: "package.json \"bin\": " + binNames.join(", ") }); | ||
| } | ||
|
|
||
| if (pkg.main || pkg.exports) { | ||
| signals.push({ name: "Publishable library", evidence: "package.json has a \"" + (pkg.exports ? "exports" : "main") + "\" field" }); | ||
| } | ||
|
|
||
| const workspacePackages = getWorkspacePackages(rootDir); | ||
| if (workspacePackages.length > 0) { | ||
| signals.push({ | ||
| name: "Monorepo (npm workspaces)", | ||
| evidence: workspacePackages.length + " workspace package(s): " + workspacePackages.map(function (p) { return p.name; }).join(", "), | ||
| }); | ||
| } | ||
|
|
||
| const dependencyFrameworks = getFrameworkSignals(rootDir).filter(function (s) { return s.confidence === "dependency"; }); | ||
| const dependencyNames = dependencyFrameworks.map(function (s) { return s.name; }); | ||
|
|
||
| const backendFrameworks = dependencyNames.filter(function (n) { return n === "Express" || n === "NestJS"; }); | ||
| if (backendFrameworks.length > 0) { | ||
| signals.push({ name: "Backend/API service", evidence: "framework dependency: " + backendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const frontendFrameworks = dependencyNames.filter(function (n) { return ["React", "Next.js", "Angular", "Vue"].indexOf(n) !== -1; }); | ||
| if (frontendFrameworks.length > 0) { | ||
| signals.push({ name: "Frontend application", evidence: "framework dependency: " + frontendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const dirNames = collectDirectoryNamesFromStructure(getProjectStructure(rootDir)); | ||
|
|
||
| const matchedMvc = MVC_DIR_NAMES.filter(function (n) { return dirNames.has(n); }); | ||
| if (matchedMvc.length >= 2) { | ||
| signals.push({ name: "MVC-influenced layout", evidence: "directories present: " + matchedMvc.join(", ") }); | ||
| } | ||
|
|
||
| // One signal, not one per group: the rule is "group A matches OR | ||
| // group B matches", not two independent observations. If both | ||
| // groups happen to match, report both as evidence on the single | ||
| // signal rather than emitting a duplicate-named entry. | ||
| const matchedLayeredGroups = LAYERED_DIR_NAME_GROUPS | ||
| .map(function (group) { return group.filter(function (n) { return dirNames.has(n); }); }) | ||
| .filter(function (matched) { return matched.length >= 2; }); | ||
| if (matchedLayeredGroups.length > 0) { | ||
| signals.push({ | ||
| name: "Layered/service-oriented layout", | ||
| evidence: "directories present: " + matchedLayeredGroups.map(function (g) { return g.join(", "); }).join(" / "), | ||
| }); | ||
| } | ||
|
|
||
| return signals; | ||
| } |
Comment on lines
+397
to
+453
| function getArchitectureSignals(rootDir) { | ||
| const signals = []; | ||
| const pkg = readJsonSafe(path.join(rootDir, "package.json"), {}); | ||
|
|
||
| if (pkg.bin && (typeof pkg.bin === "string" || (typeof pkg.bin === "object" && Object.keys(pkg.bin).length > 0))) { | ||
| const binNames = typeof pkg.bin === "string" ? [path.basename(rootDir)] : Object.keys(pkg.bin); | ||
| signals.push({ name: "CLI tool", evidence: "package.json \"bin\": " + binNames.join(", ") }); | ||
| } | ||
|
|
||
| if (pkg.main || pkg.exports) { | ||
| signals.push({ name: "Publishable library", evidence: "package.json has a \"" + (pkg.exports ? "exports" : "main") + "\" field" }); | ||
| } | ||
|
|
||
| const workspacePackages = getWorkspacePackages(rootDir); | ||
| if (workspacePackages.length > 0) { | ||
| signals.push({ | ||
| name: "Monorepo (npm workspaces)", | ||
| evidence: workspacePackages.length + " workspace package(s): " + workspacePackages.map(function (p) { return p.name; }).join(", "), | ||
| }); | ||
| } | ||
|
|
||
| const dependencyFrameworks = getFrameworkSignals(rootDir).filter(function (s) { return s.confidence === "dependency"; }); | ||
| const dependencyNames = dependencyFrameworks.map(function (s) { return s.name; }); | ||
|
|
||
| const backendFrameworks = dependencyNames.filter(function (n) { return n === "Express" || n === "NestJS"; }); | ||
| if (backendFrameworks.length > 0) { | ||
| signals.push({ name: "Backend/API service", evidence: "framework dependency: " + backendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const frontendFrameworks = dependencyNames.filter(function (n) { return ["React", "Next.js", "Angular", "Vue"].indexOf(n) !== -1; }); | ||
| if (frontendFrameworks.length > 0) { | ||
| signals.push({ name: "Frontend application", evidence: "framework dependency: " + frontendFrameworks.join(", ") }); | ||
| } | ||
|
|
||
| const dirNames = collectDirectoryNamesFromStructure(getProjectStructure(rootDir)); | ||
|
|
||
| const matchedMvc = MVC_DIR_NAMES.filter(function (n) { return dirNames.has(n); }); | ||
| if (matchedMvc.length >= 2) { | ||
| signals.push({ name: "MVC-influenced layout", evidence: "directories present: " + matchedMvc.join(", ") }); | ||
| } | ||
|
|
||
| // One signal, not one per group: the rule is "group A matches OR | ||
| // group B matches", not two independent observations. If both | ||
| // groups happen to match, report both as evidence on the single | ||
| // signal rather than emitting a duplicate-named entry. | ||
| const matchedLayeredGroups = LAYERED_DIR_NAME_GROUPS | ||
| .map(function (group) { return group.filter(function (n) { return dirNames.has(n); }); }) | ||
| .filter(function (matched) { return matched.length >= 2; }); | ||
| if (matchedLayeredGroups.length > 0) { | ||
| signals.push({ | ||
| name: "Layered/service-oriented layout", | ||
| evidence: "directories present: " + matchedLayeredGroups.map(function (g) { return g.join(", "); }).join(" / "), | ||
| }); | ||
| } | ||
|
|
||
| return signals; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds
getArchitectureSignals()tolib/project-facts.js: 7 deterministic,evidence-backed architecture-pattern signals (
CLI tool,Publishable library,Monorepo (npm workspaces),Backend/API service,Frontend application,MVC-influenced layout,Layered/service-oriented layout).Every signal is
{ name, evidence }— multiple signals commonly co-occurby design; there is deliberately no single "the architecture is X" verdict
(this repo itself is CLI tool + library + monorepo simultaneously — a
forced single label would be wrong for the exact repo this ships in).
Backend/API service/Frontend applicationonly key off Phase 2 signalsat
confidence: "dependency"— afile-heuristic-only framework guessnever promotes into an architecture claim. Zero new filesystem calls —
every rule reuses Phase 1 (
getProjectStructure,getWorkspacePackages)and Phase 2 (
getFrameworkSignals) output.This is the highest-risk phase per the epic (false-positive claims
undercut the "no AI, no surprises" trust moat), so it was gated behind
adr-architecture-pattern-signals.mdbefore any code was written — seethat ADR's Alternatives-considered section for why a single-label or
scored-confidence design was rejected.
Implements Phase 3 (
task-arch-03) ofepic-architecture-insight-report,on top of Phase 1 (#5) and Phase 2 (merged).
Related issue
Ref:
docs/backlog/epic-architecture-insight-report.md,docs/backlog/story-architecture-pattern-signals.md,docs/backlog/adr-architecture-pattern-signals.md,docs/backlog/task-arch-03-pattern-signals.mdChecklist
npm testpasses locally — full suite: 283 tests passed, 0 failures(includes 11 new
getArchitectureSignalstests;project-factssuite total 38/38)
including one per rule firing independently, one for multi-signal
co-occurrence, one proving file-heuristic signals don't promote to
architecture claims, and an exact-signal assertion against this
repo's own real facts (verified against
find . -mindepth 1 -maxdepth 3 -type doutput before writing the assertion, notassumed —
sample/express/routes|servicesgenuinely triggers thelayered-layout signal on this repo)
timestamps/randomness — pure
fs/JSON.parsereads, zero newfilesystem traversal)
CHANGELOG.md— not done, same reasoning as Phase 1/2: noconsumer wires this into CLI/generated output yet. Worth revisiting
once all 3 phases are ready to surface in
gen-docsoutput.