qaas-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qaas/adapters/__init__.py +19 -0
- qaas/adapters/tracker.py +1783 -0
- qaas/adapters/vcs.py +555 -0
- qaas/cli.py +1757 -0
- qaas/config.py +409 -0
- qaas/defaults/config/agents/api.yaml +18 -0
- qaas/defaults/config/agents/architect.yaml +21 -0
- qaas/defaults/config/agents/auditor.yaml +19 -0
- qaas/defaults/config/agents/browser.yaml +15 -0
- qaas/defaults/config/agents/dba.yaml +20 -0
- qaas/defaults/config/agents/fixer.yaml +55 -0
- qaas/defaults/config/agents/guide.yaml +23 -0
- qaas/defaults/config/agents/load.yaml +26 -0
- qaas/defaults/config/agents/mapper.yaml +19 -0
- qaas/defaults/config/agents/reporter.yaml +19 -0
- qaas/defaults/config/agents/reproducer.yaml +21 -0
- qaas/defaults/config/agents/reviewer.yaml +18 -0
- qaas/defaults/config/agents/socket.yaml +23 -0
- qaas/defaults/config/agents/triage.yaml +20 -0
- qaas/defaults/config/agents/verifier.yaml +20 -0
- qaas/defaults/config/system.yaml +64 -0
- qaas/discover.py +242 -0
- qaas/envelope.py +318 -0
- qaas/envfile.py +100 -0
- qaas/guardrails.py +589 -0
- qaas/mcp/__init__.py +0 -0
- qaas/mcp/context.py +78 -0
- qaas/mcp/contract_diff.py +1011 -0
- qaas/mcp/defect_memory.py +495 -0
- qaas/mcp/env_control.py +925 -0
- qaas/mcp/envelope_server.py +463 -0
- qaas/mcp/test_runner.py +842 -0
- qaas/mcp/tracker.py +420 -0
- qaas/mcp/vcs.py +501 -0
- qaas/paths.py +317 -0
- qaas/plugin/.claude-plugin/plugin.json +9 -0
- qaas/plugin/skills/a11y-audit/SKILL.md +34 -0
- qaas/plugin/skills/adversarial-review/SKILL.md +120 -0
- qaas/plugin/skills/api-surface-extraction/SKILL.md +38 -0
- qaas/plugin/skills/authz-matrix-check/SKILL.md +46 -0
- qaas/plugin/skills/console-error-triage/SKILL.md +39 -0
- qaas/plugin/skills/contract-test-generation/SKILL.md +36 -0
- qaas/plugin/skills/dedupe-strategy/SKILL.md +39 -0
- qaas/plugin/skills/environment-pinning/SKILL.md +35 -0
- qaas/plugin/skills/error-taxonomy/SKILL.md +42 -0
- qaas/plugin/skills/exploratory-ui-walk/SKILL.md +46 -0
- qaas/plugin/skills/failing-test-authoring/SKILL.md +47 -0
- qaas/plugin/skills/flake-detection/SKILL.md +39 -0
- qaas/plugin/skills/form-state-probe/SKILL.md +36 -0
- qaas/plugin/skills/minimal-diff-discipline/SKILL.md +70 -0
- qaas/plugin/skills/openapi-diff/SKILL.md +45 -0
- qaas/plugin/skills/ownership-resolution/SKILL.md +31 -0
- qaas/plugin/skills/product-task-graph/SKILL.md +35 -0
- qaas/plugin/skills/regression-risk-scoring/SKILL.md +59 -0
- qaas/plugin/skills/regression-suite-selection/SKILL.md +36 -0
- qaas/plugin/skills/repo-cartography/SKILL.md +38 -0
- qaas/plugin/skills/repro-minimisation/SKILL.md +41 -0
- qaas/plugin/skills/rollback-plan-authoring/SKILL.md +81 -0
- qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +67 -0
- qaas/plugin/skills/routing-rules/SKILL.md +34 -0
- qaas/plugin/skills/severity-rubric/SKILL.md +42 -0
- qaas/plugin/skills/test-first-fix/SKILL.md +66 -0
- qaas/plugin/skills/test-quality-audit/SKILL.md +58 -0
- qaas/plugin/skills/ticket-writer/SKILL.md +40 -0
- qaas/plugin/skills/verdict-reporting/SKILL.md +35 -0
- qaas/plugin/skills/verification-protocol/SKILL.md +39 -0
- qaas/prompts/API.md +44 -0
- qaas/prompts/ARCHITECT.md +80 -0
- qaas/prompts/AUDITOR.md +62 -0
- qaas/prompts/BROWSER.md +46 -0
- qaas/prompts/DBA.md +59 -0
- qaas/prompts/FIXER.md +55 -0
- qaas/prompts/GUIDE.md +94 -0
- qaas/prompts/LOAD.md +109 -0
- qaas/prompts/MAPPER.md +46 -0
- qaas/prompts/REPORTER.md +61 -0
- qaas/prompts/REPRODUCER.md +43 -0
- qaas/prompts/REVIEWER.md +53 -0
- qaas/prompts/SOCKET.md +100 -0
- qaas/prompts/TRIAGE.md +45 -0
- qaas/prompts/VERIFIER.md +41 -0
- qaas/prompts/_shared.md +45 -0
- qaas/registry.py +496 -0
- qaas/router.py +581 -0
- qaas/runner.py +210 -0
- qaas/scorecard.py +448 -0
- qaas/sdk_compat.py +52 -0
- qaas/store.py +323 -0
- qaas/target.py +287 -0
- qaas/tasks.py +438 -0
- qaas/trace.py +342 -0
- qaas_python-0.0.1.dist-info/METADATA +429 -0
- qaas_python-0.0.1.dist-info/RECORD +96 -0
- qaas_python-0.0.1.dist-info/WHEEL +4 -0
- qaas_python-0.0.1.dist-info/entry_points.txt +2 -0
- qaas_python-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: REVIEWER
|
|
2
|
+
layer: remediation
|
|
3
|
+
role: >
|
|
4
|
+
Review and risk gate. Reads FIXER's diff as an adversarial reviewer and judges
|
|
5
|
+
correctness, scope creep, hidden regressions and risk. Separate from FIXER for
|
|
6
|
+
the same reason discovery is separate from triage: a model reviewing its own
|
|
7
|
+
diff in the same context reliably rationalises it.
|
|
8
|
+
prompt: REVIEWER.md
|
|
9
|
+
model: claude-opus-5
|
|
10
|
+
effort: high
|
|
11
|
+
max_turns: 50
|
|
12
|
+
mcp_servers: [envelope, vcs, contract_diff, test_runner]
|
|
13
|
+
builtin_tools: [Read, Grep, Glob]
|
|
14
|
+
skills: [adversarial-review, root-cause-vs-symptom, regression-risk-scoring, test-quality-audit]
|
|
15
|
+
|
|
16
|
+
policy: {} # no write access to code, ever — the whole point of the separation
|
|
17
|
+
|
|
18
|
+
must_call: [mcp__envelope__record_review]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: SOCKET
|
|
2
|
+
layer: discovery
|
|
3
|
+
role: >
|
|
4
|
+
Realtime and WebSocket analyst. Finds auth bypass on the upgrade handshake,
|
|
5
|
+
reconnect without jittered backoff, message loss with no resume token,
|
|
6
|
+
order-dependent consumers with nothing carrying order, missing heartbeats,
|
|
7
|
+
absent backpressure, and channel authorization never re-checked after
|
|
8
|
+
subscribe. The WebSocket harness the design gives this role does not exist
|
|
9
|
+
here, so SOCKET reads the connection code and reports at the confidence of a
|
|
10
|
+
source reading, not of a measurement.
|
|
11
|
+
prompt: SOCKET.md
|
|
12
|
+
model: claude-opus-5
|
|
13
|
+
effort: high
|
|
14
|
+
max_turns: 60
|
|
15
|
+
mcp_servers: [envelope, env_control, defect_memory]
|
|
16
|
+
builtin_tools: [Read, Grep, Glob]
|
|
17
|
+
policy: {} # read-only
|
|
18
|
+
|
|
19
|
+
skills: [api-surface-extraction, authz-matrix-check, environment-pinning, severity-rubric]
|
|
20
|
+
|
|
21
|
+
# No must_call: see DBA. It matters more here than anywhere -- a target with no
|
|
22
|
+
# realtime surface should produce zero envelopes, and a required emission would
|
|
23
|
+
# turn "there are no websockets" into a manufactured finding about one.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: TRIAGE
|
|
2
|
+
layer: triage
|
|
3
|
+
role: >
|
|
4
|
+
Triage and ticket scribe. The only agent with tracker write access. Dedupes,
|
|
5
|
+
scores severity against the rubric, resolves ownership from the system map,
|
|
6
|
+
routes by class, and files.
|
|
7
|
+
prompt: TRIAGE.md
|
|
8
|
+
model: claude-sonnet-5 # composition against a fixed rubric and house format
|
|
9
|
+
effort: high
|
|
10
|
+
max_turns: 50
|
|
11
|
+
mcp_servers: [envelope, tracker, defect_memory]
|
|
12
|
+
builtin_tools: [Read]
|
|
13
|
+
policy:
|
|
14
|
+
may_create_tickets: true
|
|
15
|
+
max_tickets_per_run: 10 # §4.12: over cap, pause and escalate
|
|
16
|
+
|
|
17
|
+
skills: [severity-rubric, dedupe-strategy, ticket-writer, routing-rules, ownership-resolution]
|
|
18
|
+
|
|
19
|
+
# Filing nothing is allowed; filing without checking for duplicates is not.
|
|
20
|
+
must_call: [mcp__defect_memory__search_similar]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: VERIFIER
|
|
2
|
+
layer: remediation
|
|
3
|
+
role: >
|
|
4
|
+
Verification and regression gate, and the closing authority. Re-runs the
|
|
5
|
+
original failing test against the patched build, runs the regression suite for
|
|
6
|
+
affected areas, and returns VERIFIED, NOT_FIXED or REGRESSED.
|
|
7
|
+
prompt: VERIFIER.md
|
|
8
|
+
model: claude-opus-5
|
|
9
|
+
effort: high
|
|
10
|
+
max_turns: 60
|
|
11
|
+
mcp_servers: [envelope, test_runner, env_control, tracker, vcs]
|
|
12
|
+
builtin_tools: [Read, Grep, Glob, Bash]
|
|
13
|
+
policy:
|
|
14
|
+
may_transition_tickets: true # transition only, never create (§8.1)
|
|
15
|
+
|
|
16
|
+
skills: [verification-protocol, regression-suite-selection, verdict-reporting, severity-rubric]
|
|
17
|
+
|
|
18
|
+
# A verification that ends without transitioning the ticket has left the ticket
|
|
19
|
+
# in review forever, which is the one outcome worse than a wrong verdict.
|
|
20
|
+
must_call: [mcp__envelope__record_verdict, mcp__tracker__transition]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# System configuration. Run modes follow architecture §9; the Phase 1 roster is
|
|
2
|
+
# the six agents from §11, so modes name only those until Phase 2 lands.
|
|
3
|
+
project: qaas
|
|
4
|
+
# Which target profile to run against, from <config>/targets/<name>.yaml.
|
|
5
|
+
# Unset means "not pointed at anything yet", which is exactly what a fresh
|
|
6
|
+
# `pip install` is. `qaas init` writes this for you; QAAS_TARGET overrides it.
|
|
7
|
+
target:
|
|
8
|
+
|
|
9
|
+
tracker: local # local | jira -- swap to file against real Jira
|
|
10
|
+
vcs: local # local | github
|
|
11
|
+
|
|
12
|
+
# No spend cap by default. A dollar figure bakes one vendor's pricing into
|
|
13
|
+
# the config, and this is meant to run against local models too. `max_turns`
|
|
14
|
+
# is the model-agnostic bound. Set `max_budget_usd` here if you want a
|
|
15
|
+
# ceiling; the governor enforces one whenever it is present.
|
|
16
|
+
|
|
17
|
+
thresholds:
|
|
18
|
+
min_confidence_to_file: 0.6 # §7 confidence gate
|
|
19
|
+
max_findings_per_agent_run: 25 # §8.3 loop breaker: pause and escalate, don't file
|
|
20
|
+
max_tickets_per_run: 10 # §4.12 rate limit
|
|
21
|
+
flake_runs: 5 # REPRODUCER runs a repro N times to measure flake
|
|
22
|
+
max_mender_arbiter_round_trips: 2
|
|
23
|
+
max_proof_reopens: 1
|
|
24
|
+
|
|
25
|
+
run_modes:
|
|
26
|
+
pr-check:
|
|
27
|
+
trigger: pull_request
|
|
28
|
+
# ARCHITECT joins the PR sweep because it is pure static analysis and needs
|
|
29
|
+
# no running app. GUIDE, LOAD and REPORTER do not: the design says LOAD is
|
|
30
|
+
# "nightly and pre-release only -- too expensive and too noisy per-PR", and
|
|
31
|
+
# the same argument holds for a UX walk and a report about the week.
|
|
32
|
+
agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, REPRODUCER, TRIAGE]
|
|
33
|
+
max_wall_clock_s: 900
|
|
34
|
+
max_concurrency: 2
|
|
35
|
+
|
|
36
|
+
nightly:
|
|
37
|
+
trigger: cron
|
|
38
|
+
agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, SOCKET, GUIDE, LOAD, REPRODUCER, TRIAGE, REPORTER]
|
|
39
|
+
max_wall_clock_s: 7200
|
|
40
|
+
max_concurrency: 3
|
|
41
|
+
|
|
42
|
+
incident:
|
|
43
|
+
trigger: alert
|
|
44
|
+
agents: [API]
|
|
45
|
+
max_wall_clock_s: 600
|
|
46
|
+
max_concurrency: 2
|
|
47
|
+
files_tickets: false # §9: diagnostic only, read-only, no filing
|
|
48
|
+
|
|
49
|
+
# §9: the remediation loop. VERIFIER verifies first — a ticket whose defect no
|
|
50
|
+
# longer reproduces needs no fix, and finding that out costs one cheap run
|
|
51
|
+
# instead of a whole FIXER/REVIEWER cycle.
|
|
52
|
+
fix-cycle:
|
|
53
|
+
trigger: agent_ready_ticket
|
|
54
|
+
agents: [VERIFIER, FIXER, REVIEWER]
|
|
55
|
+
max_wall_clock_s: 3600
|
|
56
|
+
max_concurrency: 1
|
|
57
|
+
|
|
58
|
+
# Everything, end to end: discover, triage, file, fix, review, verify. The most
|
|
59
|
+
# expensive mode in the system and the only one that closes the loop.
|
|
60
|
+
full-loop:
|
|
61
|
+
trigger: on_demand
|
|
62
|
+
agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, SOCKET, GUIDE, LOAD, REPRODUCER, TRIAGE, FIXER, REVIEWER, VERIFIER, REPORTER]
|
|
63
|
+
max_wall_clock_s: 10800
|
|
64
|
+
max_concurrency: 3
|
qaas/discover.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Inspecting an unfamiliar repository well enough to write a target profile.
|
|
2
|
+
|
|
3
|
+
This is deliberately dumb pattern-matching, not analysis. Its job is to save a
|
|
4
|
+
person ten minutes of typing and to be obviously wrong when it is wrong — every
|
|
5
|
+
value it produces is a guess a human is expected to correct, and `qaas init`
|
|
6
|
+
says so. MAPPER does the real mapping later, with a model and the whole
|
|
7
|
+
repository in front of it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from qaas.target import DEFAULT_EXCLUDES, Auth, Environment, Layout, Role, TargetProfile
|
|
18
|
+
|
|
19
|
+
SPEC_NAMES = [
|
|
20
|
+
"openapi.yaml", "openapi.yml", "openapi.json",
|
|
21
|
+
"swagger.yaml", "swagger.yml", "swagger.json",
|
|
22
|
+
"api/openapi.yaml", "docs/openapi.yaml", "spec/openapi.yaml",
|
|
23
|
+
]
|
|
24
|
+
OWNERSHIP_NAMES = ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS", ".gitlab/CODEOWNERS"]
|
|
25
|
+
COMPOSE_NAMES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]
|
|
26
|
+
|
|
27
|
+
BACKEND_MARKERS = {
|
|
28
|
+
"python": ("requirements.txt", "pyproject.toml", "manage.py", "Pipfile"),
|
|
29
|
+
"node": ("package.json",),
|
|
30
|
+
"go": ("go.mod",),
|
|
31
|
+
"java": ("pom.xml", "build.gradle", "build.gradle.kts"),
|
|
32
|
+
"ruby": ("Gemfile",),
|
|
33
|
+
"rust": ("Cargo.toml",),
|
|
34
|
+
"php": ("composer.json",),
|
|
35
|
+
}
|
|
36
|
+
BACKEND_HINTS = re.compile(
|
|
37
|
+
r"\b(fastapi|flask|django|express|nestjs|gin|echo|spring|rails|sinatra|laravel|actix|axum)\b",
|
|
38
|
+
re.I,
|
|
39
|
+
)
|
|
40
|
+
FRONTEND_HINTS = re.compile(r'"(react|vue|svelte|@angular/core|next|nuxt|solid-js)"', re.I)
|
|
41
|
+
|
|
42
|
+
TEST_DIR_NAMES = {"tests", "test", "__tests__", "spec", "e2e", "integration_tests"}
|
|
43
|
+
MIGRATION_DIR_NAMES = {"migrations", "migrate", "alembic", "db/migrate", "prisma/migrations"}
|
|
44
|
+
|
|
45
|
+
#: `Layout.docs` existed and nothing ever filled it, so a documentation-heavy
|
|
46
|
+
#: repository profiled as having no documentation at all and ARCHITECT was told
|
|
47
|
+
#: to go and find it. Prose is a real surface: specs that contradict the code,
|
|
48
|
+
#: tickets that describe behaviour nobody built, standards nothing follows.
|
|
49
|
+
DOC_DIR_NAMES = {"docs", "doc", "documentation", "adr", "rfcs", "specs"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Discovery:
|
|
54
|
+
"""What inspection found, with a note on anything it could not settle."""
|
|
55
|
+
|
|
56
|
+
layout: Layout
|
|
57
|
+
environment: Environment
|
|
58
|
+
auth: Auth
|
|
59
|
+
languages: set[str] = field(default_factory=set)
|
|
60
|
+
notes: list[str] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _rel(path: Path, root: Path) -> str:
|
|
64
|
+
return str(path.relative_to(root)).replace("\\", "/")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _walk(root: Path, excludes: set[str], max_depth: int = 4):
|
|
68
|
+
"""Directories worth looking at, breadth-first, skipping vendored trees."""
|
|
69
|
+
stack = [(root, 0)]
|
|
70
|
+
while stack:
|
|
71
|
+
current, depth = stack.pop()
|
|
72
|
+
if depth > max_depth:
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
children = list(current.iterdir())
|
|
76
|
+
except (PermissionError, OSError):
|
|
77
|
+
continue
|
|
78
|
+
yield current, children
|
|
79
|
+
for child in children:
|
|
80
|
+
if child.is_dir() and child.name not in excludes and not child.name.startswith("."):
|
|
81
|
+
stack.append((child, depth + 1))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _has_component_source(path: Path, excludes: set[str]) -> bool:
|
|
85
|
+
"""Whether a directory holds component source of its own.
|
|
86
|
+
|
|
87
|
+
Deliberately not `rglob`: that descends into `node_modules`, where a great
|
|
88
|
+
many packages ship .tsx, so it answers yes for almost any directory in a
|
|
89
|
+
JavaScript repository — and walks a vendored tree to get there. `_walk`
|
|
90
|
+
honours the same exclusions as the rest of this module.
|
|
91
|
+
"""
|
|
92
|
+
return any(
|
|
93
|
+
c.suffix in {".tsx", ".jsx"}
|
|
94
|
+
for _, children in _walk(path, excludes)
|
|
95
|
+
for c in children
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def inspect(root: Path) -> Discovery:
|
|
100
|
+
"""Guess a repository's shape. Every field is a hint, not a conclusion."""
|
|
101
|
+
root = root.resolve()
|
|
102
|
+
excludes = set(DEFAULT_EXCLUDES)
|
|
103
|
+
notes: list[str] = []
|
|
104
|
+
languages: set[str] = set()
|
|
105
|
+
|
|
106
|
+
backend: list[str] = []
|
|
107
|
+
frontend: list[str] = []
|
|
108
|
+
tests: list[str] = []
|
|
109
|
+
migrations: list[str] = []
|
|
110
|
+
docs: list[str] = []
|
|
111
|
+
|
|
112
|
+
for directory, children in _walk(root, excludes):
|
|
113
|
+
names = {c.name for c in children}
|
|
114
|
+
rel = _rel(directory, root) if directory != root else "."
|
|
115
|
+
|
|
116
|
+
for language, markers in BACKEND_MARKERS.items():
|
|
117
|
+
if names & set(markers):
|
|
118
|
+
languages.add(language)
|
|
119
|
+
if language == "node" and (directory / "package.json").exists():
|
|
120
|
+
# package.json alone says nothing; the dependencies do.
|
|
121
|
+
try:
|
|
122
|
+
pkg = (directory / "package.json").read_text(encoding="utf-8")
|
|
123
|
+
except OSError:
|
|
124
|
+
pkg = ""
|
|
125
|
+
if FRONTEND_HINTS.search(pkg):
|
|
126
|
+
frontend.append(rel)
|
|
127
|
+
continue
|
|
128
|
+
if BACKEND_HINTS.search(pkg):
|
|
129
|
+
backend.append(rel)
|
|
130
|
+
continue
|
|
131
|
+
elif rel not in backend:
|
|
132
|
+
backend.append(rel)
|
|
133
|
+
|
|
134
|
+
if directory.name in TEST_DIR_NAMES and rel not in tests:
|
|
135
|
+
tests.append(rel)
|
|
136
|
+
if directory.name in MIGRATION_DIR_NAMES and rel not in migrations:
|
|
137
|
+
migrations.append(rel)
|
|
138
|
+
# Only the top of a documentation tree: `docs` and `docs/specs` and
|
|
139
|
+
# `docs/tickets` are one surface, and listing all three says nothing
|
|
140
|
+
# the first does not.
|
|
141
|
+
if directory.name in DOC_DIR_NAMES and not any(
|
|
142
|
+
rel == d or rel.startswith(f"{d}/") for d in docs
|
|
143
|
+
):
|
|
144
|
+
docs.append(rel)
|
|
145
|
+
|
|
146
|
+
# A source directory with .tsx/.jsx in it is a frontend even without a
|
|
147
|
+
# package.json of its own — monorepos often hoist dependencies.
|
|
148
|
+
if not frontend:
|
|
149
|
+
for candidate in ("web", "frontend", "client", "ui", "app"):
|
|
150
|
+
path = root / candidate
|
|
151
|
+
if path.is_dir() and _has_component_source(path, excludes):
|
|
152
|
+
frontend.append(candidate)
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
spec = next((s for s in SPEC_NAMES if (root / s).exists()), None)
|
|
156
|
+
ownership = next((o for o in OWNERSHIP_NAMES if (root / o).exists()), None)
|
|
157
|
+
compose = next((c for c in COMPOSE_NAMES if (root / c).exists()), None)
|
|
158
|
+
|
|
159
|
+
if not spec:
|
|
160
|
+
notes.append(
|
|
161
|
+
"No OpenAPI document found. API can still audit the API, but it has "
|
|
162
|
+
"no declared contract to diff against — set layout.spec if one exists "
|
|
163
|
+
"somewhere this did not look."
|
|
164
|
+
)
|
|
165
|
+
if not ownership:
|
|
166
|
+
notes.append(
|
|
167
|
+
"No CODEOWNERS file. Tickets will be filed unassigned unless you add one "
|
|
168
|
+
"or set layout.ownership."
|
|
169
|
+
)
|
|
170
|
+
if not backend and not frontend:
|
|
171
|
+
notes.append(
|
|
172
|
+
"Could not identify backend or frontend directories. Set layout.backend "
|
|
173
|
+
"and layout.frontend by hand — leaving them empty makes MAPPER "
|
|
174
|
+
"explore blind, which is slower and less accurate."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
environment = Environment(mode="none")
|
|
178
|
+
if compose:
|
|
179
|
+
notes.append(
|
|
180
|
+
f"Found {compose}. Environment mode is still 'none': review the compose "
|
|
181
|
+
"file, then set mode to 'compose' with the service names and URLs. This "
|
|
182
|
+
"is not switched on automatically because bringing up someone's stack "
|
|
183
|
+
"unasked is not a decision a tool should make."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# "." is not a service. A root pyproject.toml or package.json is usually
|
|
187
|
+
# tooling or workspace config, and listing the root as a backend directory
|
|
188
|
+
# tells MAPPER nothing while making it read the whole repository.
|
|
189
|
+
if len(set(backend)) > 1:
|
|
190
|
+
backend = [b for b in backend if b != "."]
|
|
191
|
+
|
|
192
|
+
return Discovery(
|
|
193
|
+
layout=Layout(
|
|
194
|
+
backend=sorted(set(backend))[:6],
|
|
195
|
+
frontend=sorted(set(frontend))[:4],
|
|
196
|
+
tests=sorted(set(tests))[:4],
|
|
197
|
+
migrations=sorted(set(migrations))[:3],
|
|
198
|
+
docs=sorted(set(docs))[:4],
|
|
199
|
+
spec=spec,
|
|
200
|
+
ownership=ownership,
|
|
201
|
+
exclude=list(DEFAULT_EXCLUDES),
|
|
202
|
+
),
|
|
203
|
+
environment=environment,
|
|
204
|
+
auth=Auth(mode="none"),
|
|
205
|
+
languages=languages,
|
|
206
|
+
notes=notes,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_profile(
|
|
211
|
+
name: str,
|
|
212
|
+
root: Path,
|
|
213
|
+
*,
|
|
214
|
+
repo_url: str | None = None,
|
|
215
|
+
description: str = "",
|
|
216
|
+
default_branch: str = "main",
|
|
217
|
+
) -> tuple[TargetProfile, list[str]]:
|
|
218
|
+
found = inspect(root)
|
|
219
|
+
profile = TargetProfile(
|
|
220
|
+
name=name,
|
|
221
|
+
root=str(root),
|
|
222
|
+
repo_url=repo_url,
|
|
223
|
+
description=description or _describe(root, found),
|
|
224
|
+
default_branch=default_branch,
|
|
225
|
+
layout=found.layout,
|
|
226
|
+
environment=found.environment,
|
|
227
|
+
auth=found.auth,
|
|
228
|
+
)
|
|
229
|
+
return profile, found.notes
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _describe(root: Path, found: Discovery) -> str:
|
|
233
|
+
langs = ", ".join(sorted(found.languages)) or "unknown stack"
|
|
234
|
+
readme = next((root / n for n in ("README.md", "readme.md", "README.rst") if (root / n).exists()), None)
|
|
235
|
+
first_line = ""
|
|
236
|
+
if readme:
|
|
237
|
+
for line in readme.read_text(errors="ignore").splitlines():
|
|
238
|
+
stripped = line.strip().lstrip("#").strip()
|
|
239
|
+
if stripped and not stripped.startswith(("!", "[", "<")):
|
|
240
|
+
first_line = stripped
|
|
241
|
+
break
|
|
242
|
+
return f"{first_line} ({langs})".strip() if first_line else f"A {langs} project."
|