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: 3 additions & 1 deletion .claude/agents/squad-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ derived values and expect the Tester's test that pins the claim. A security prop
rests on measured or library behavior (a deadline that interrupts a query step, a limit a parser enforces, a
server's flush semantics) needs its measurement (`.squad/team.md`, *Test doubles that model a real
component*): in mode `plan` you run it once before you approve, in mode `diff` you check the Dev's; an
unverified claim of that kind is a blocking finding.
unverified claim of that kind is a blocking finding. If the measurement cannot run because a download or tool
is denied, report it as not run and name what it needs; the orchestrator handles it (`.squad/orchestration.md`,
*A denied measurement*).

Rules:

Expand Down
14 changes: 6 additions & 8 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/install-linters.sh"

cd "${CLAUDE_PROJECT_DIR:-$(pwd)}"

# .NET global tools (reihitsu-format) need DOTNET_ROOT when dotnet is not installed in a default location.
# .NET tools (reihitsu-format) need DOTNET_ROOT when dotnet is not installed in a default location.
dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)")")"
export DOTNET_ROOT="$dotnet_root"
export PATH="$PATH:$HOME/.dotnet/tools"
Expand All @@ -23,14 +23,12 @@ if [[ -n "${CLAUDE_ENV_FILE:-}" ]]; then
} >> "$CLAUDE_ENV_FILE"
fi

# Formatter (*Format* in .squad/stack.md), run by the squad member that changed the files
if ! command -v reihitsu-format >/dev/null 2>&1; then
dotnet tool install -g Reihitsu.Cli
fi

# Local tools of the repository's manifest (dotnet-sonarscanner for the *Sonar gate*, a formatter pinned there).
# Local tools of the repository's manifest: the formatter (*Format* in .squad/stack.md) and dotnet-sonarscanner
# (*Sonar gate*), both pinned there.
if [[ -e dotnet-tools.json || -e .config/dotnet-tools.json ]]; then
dotnet tool restore || echo "dotnet tool restore failed; run it before the Sonar gate" >&2
dotnet tool restore || echo "dotnet tool restore failed; run it before Format and the Sonar gate" >&2
else
echo "no dotnet tool manifest (.config/dotnet-tools.json); the formatter is not installed" >&2
fi

# Restore every solution at the repository root (the squad's settings name the one the gates build).
Expand Down
7 changes: 7 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
"version": 1,
"isRoot": true,
"tools": {
"reihitsu.cli": {
"version": "1.1.0",
"commands": [
"reihitsu-format"
],
"rollForward": false
},
"dotnet-sonarscanner": {
"version": "12.0.0",
"commands": [
Expand Down
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ Run from the repository root, where the solution file lives, in this order: *Res
*Test*, *Analyzer gate*, and after *Test with coverage* the *Coverage gate*. Use the commands of those names
in `.squad/stack.md` as they stand, never a copy from memory: they carry the solution name, the build
configuration the project tests in, and the formatter's `--force` (`reihitsu-format` asks for confirmation
for more than 25 files and, without a terminal, formats nothing). The formatter is installed with
`dotnet tool install -g Reihitsu.Cli`.
for more than 25 files and, without a terminal, formats nothing). The formatter is restored from the repository's tool
manifest (`.config/dotnet-tools.json`) with `dotnet tool restore` and run as `dotnet tool run reihitsu-format`.
<!-- stack:end commands -->

All commands, with what each one checks, are listed in [`.squad/stack.md`](/.squad/stack.md).
Expand Down
8 changes: 8 additions & 0 deletions .squad/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ state verbatim, is an ordinary fix for its owner with a delta round. One excepti
statement an acceptance criterion or a guarantee in `.squad/project.md` relies on is not applied; the reviewer says
so, and the Lead decides (mode `decide`).

## A denied measurement

When `squad-security` in mode `plan` reports a required measurement as not run because a download or tool was
denied (e.g. a vendor release binary refused by the permission classifier), the orchestrator asks the Product
Manager once for exactly that download, runs the measurement itself once it is allowed, and passes the measured
result with the plan to the Lead (mode `revise`). Security re-runs the review in round 2 against the revised
plan and the measurement. Not run is never approved, and the denial alone is no reason to skip the measurement.

## Unlisted security areas

A Reviewer finding *unlisted security area* β€” the diff touches a security area the plan's *Security areas
Expand Down
9 changes: 5 additions & 4 deletions .squad/stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ profile and owned by this repository: keep it true when the build changes.
## Toolchain

- .NET SDK `10.0.x` (target framework `net10.0`), solution `DockerUpdateGuard.slnx`.
- Formatter: `reihitsu-format` (`dotnet tool install -g Reihitsu.Cli`), on the same release line as the
- Formatter: `reihitsu-format`, restored from the tool manifest `.config/dotnet-tools.json` (`dotnet tool restore`,
pinned there next to `dotnet-sonarscanner` so Dependabot raises them with the analyzer), on the same release line as the
**Reihitsu.Analyzer** version pinned in `Directory.Packages.props`; otherwise the formatter can revert
code the analyzer considers correct.
- Analyzers: **SonarAnalyzer.CSharp** in every project via `Directory.Build.props` (the Sonar C# rules locally,
so SonarQube issues surface before the push), **Reihitsu.Analyzer** likewise via `Directory.Build.props`, both
configured by the rulesets in `rules/`, with warnings treated as errors; the MSTest analyzers come with the MSTest package in the test projects.
- The SessionStart hook `.claude/hooks/session-start.sh` installs the formatter, sets `DOTNET_ROOT` and
- The SessionStart hook `.claude/hooks/session-start.sh` restores the formatter, sets `DOTNET_ROOT` and
restores the solution in remote sessions.

## Layout
Expand All @@ -30,8 +31,8 @@ profile and owned by this repository: keep it true when the build changes.
| Name | Command |
| ---- | ------- |
| *Restore* | `dotnet restore DockerUpdateGuard.slnx` |
| *Format* (in the squad: by the member that changed the files, `.squad/team.md`) | `reihitsu-format --force ./` |
| *Format check* | `reihitsu-format --check ./` |
| *Format* (in the squad: by the member that changed the files, `.squad/team.md`) | `dotnet tool run reihitsu-format --force ./` |
| *Format check* | `dotnet tool run reihitsu-format --check ./` |
| *Build* | `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` |
| *Test* | `dotnet test DockerUpdateGuard.slnx -c Release --no-build` |
| *Single test* | `dotnet test src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~ClassName.MethodName"` |
Expand Down
2 changes: 1 addition & 1 deletion .squad/template.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repository": "LarsLaskowski/Squad-Spec-Repository-Template",
"commit": "b38a99f77efcbab02f0e1c04c55f301682789111",
"commit": "cee6347c97940443a9638ec2a4fb3f47a6c400af",
"profile": "dotnet",
"profiles": [
"dotnet"
Expand Down
17 changes: 11 additions & 6 deletions .squad/tools/plan-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
r"NNNN-title", rf"\bAC\d+: {ELLIPSIS}\s*$",
rf"^\s*- [SE]\d+: {ELLIPSIS}\s*$", rf"^\|.*\|\s*{ELLIPSIS}\s*\|")]
EMPTY_CELLS = {"", "-", "\u2013", "\u2014", "...", "\u2026", "none", "n/a", "tbd", "todo"}
COMMENT = re.compile(r"<!--.*?-->", re.S)
COMMENT = re.compile(r"(```.*?```|`[^`\n]*`)|<!--.*?-->", re.S) # a code span or fenced block is kept whole
EMPHASIS = "`*_"
SPEC = "spec.md"
SPECS_PREFIX = "specs/"
Expand All @@ -87,6 +87,11 @@
TOP_ITEM = re.compile(r"^(?:[-*]|\d+[.)])\s+")


def strip_comments(text):
"""The text without its HTML comments; a comment opener inside a code span or fenced block is no comment."""
return COMMENT.sub(lambda match: match.group(1) or "", text)


def read(path, errors):
if not os.path.isfile(path):
return None
Expand Down Expand Up @@ -124,7 +129,7 @@ def ids(text, kind=None):
def sections(text):
"""{heading: body} for every `## ` section, without the template's guidance comments."""
result, current = {}, None
for line in COMMENT.sub("", text).splitlines():
for line in strip_comments(text).splitlines():
if line.startswith("## "):
current = line[3:].strip()
result[current] = []
Expand Down Expand Up @@ -175,7 +180,7 @@ def empty(value):


def check_placeholders(name, text, errors):
for line in COMMENT.sub("", text).splitlines():
for line in strip_comments(text).splitlines():
if any(pattern.search(line) for pattern in PLACEHOLDERS):
errors.append(f"{name}: template placeholder left: {line.strip()}")

Expand Down Expand Up @@ -289,7 +294,7 @@ def signature_names(body):
"""The identifier of every backticked declaration in the plan's *Signatures* section (`Options.Kernel` and
`func (o *Options) Kernel() string` give `Kernel`); a "none" section gives none."""
names = []
for span in SPAN.findall(COMMENT.sub("", body or "")):
for span in SPAN.findall(strip_comments(body or "")):
call = re.search(r"([A-Za-z_]\w*)\s*\(", span)
words = [w for w in re.findall(r"[A-Za-z_][\w.]*", span) if w not in DECLARATION_KEYWORDS]
name = words[0].split(".")[-1] if words else ""
Expand Down Expand Up @@ -399,7 +404,7 @@ def check_plan(plan, spec, errors):
if name not in TIERS or tier.group(2).lstrip().startswith("|"):
errors.append(f"plan.md: the *Tier* line must name exactly one of {', '.join(TIERS)}")
check_security_areas(plan, errors)
own = set(CRITERION.findall(COMMENT.sub("", plan)))
own = set(CRITERION.findall(strip_comments(plan)))
if spec is None:
criteria = own
if not criteria:
Expand All @@ -422,7 +427,7 @@ def check_spec_only(folder):
check_spec(spec, errors)
check_placeholders(SPEC, spec, errors)
check_control_characters(folder, errors)
words = len(COMMENT.sub("", spec).split())
words = len(strip_comments(spec).split())
if words > SPEC_MAX_WORDS:
return errors, [f"spec.md has {words} words (SPEC_MAX_WORDS {SPEC_MAX_WORDS}), and every later launch reads it: "
"keep one scenario per behavior a user can tell apart and only the edge cases that change "
Expand Down
20 changes: 13 additions & 7 deletions .squad/tools/sonar-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,16 +304,15 @@ def branch_preconditions():


def findings_on_branch(key, branch, token):
"""Open issues and hotspots of the analysed branch on the lines the branch changed."""
"""Open issues and hotspots of the analysed branch on the lines the branch changed. The issues are read for
the whole project and filtered by path here: a `componentKeys` list mixing source files (`FIL`) and test
files (`UTS`) is refused with HTTP 400."""
lines = changed_line_map(merge_base_or_raise())
paths = sorted(lines)
found = []
issues = paged("issues/search", {"componentKeys": key, "branch": branch, "resolved": "false"}, "issues", token)
found = [finding_line(i, key, "issue") for i in issues if on_changed_line(i, key, lines)]
for start in range(0, len(paths), FILES_PER_QUERY):
chunk = paths[start:start + FILES_PER_QUERY]
components = ",".join(f"{key}:{path}" for path in chunk)
issues = paged("issues/search", {"componentKeys": components, "branch": branch, "resolved": "false"},
"issues", token)
found += [finding_line(i, key, "issue") for i in issues if on_changed_line(i, key, lines)]
hotspots = paged("hotspots/search", {"projectKey": key, "branch": branch, "files": ",".join(chunk),
"status": "TO_REVIEW"}, "hotspots", token)
found += [finding_line(h, key, "hotspot") for h in hotspots if on_changed_line(h, key, lines)]
Expand All @@ -339,7 +338,14 @@ def check_branch(key, command, token):
result = wait_for_task(task, token)
if result["status"] != "SUCCESS":
raise NotChecked(f"SonarQube processed the report with status {result['status']}")
return report(findings_on_branch(key, result.get("branch") or branch, token))
try:
findings = findings_on_branch(key, result.get("branch") or branch, token)
except ApiError as error:
if "non main branch" not in str(error):
raise
raise NotChecked("the organization cannot read data of non-main branches through the API; read the "
"analysis on the draft pull request with `sonar-check.py pr <number>` in step 11") from error
return report(findings)


def parse(argv):
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ Run from the repository root, where the solution file lives, in this order: *Res
*Test*, *Analyzer gate*, and after *Test with coverage* the *Coverage gate*. Use the commands of those names
in `.squad/stack.md` as they stand, never a copy from memory: they carry the solution name, the build
configuration the project tests in, and the formatter's `--force` (`reihitsu-format` asks for confirmation
for more than 25 files and, without a terminal, formats nothing). The formatter is installed with
`dotnet tool install -g Reihitsu.Cli`.
for more than 25 files and, without a terminal, formats nothing). The formatter is restored from the repository's tool
manifest (`.config/dotnet-tools.json`) with `dotnet tool restore` and run as `dotnet tool run reihitsu-format`.
<!-- stack:end commands -->

All commands, with what each one checks, are listed in [`.squad/stack.md`](/.squad/stack.md).
Expand Down
5 changes: 3 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ When a PR is related to an issue, use the `Closes #issuenumber` syntax so the is
PR automatically and closes when the PR is merged.

The PR's label decides its section in the generated release notes (`.github/release.yml`): `breaking-change`,
`enhancement`, `security`, `bug`; any other PR is listed under *Other changes*, and `ignore-for-release` leaves it out.
`enhancement` or `feature`, `security`, `bug`; any other PR is listed under *Other changes*, and `ignore-for-release` leaves it out.

Follow the PR template in [`.github/pull_request_template.md`](../.github/pull_request_template.md).

Expand All @@ -104,7 +104,8 @@ is agreed as acceptance criteria before it is built, and the maintainers own tho
on the first two. Open an issue instead.
- **Checks on a fork:** pull requests from forks run without repository secrets, so the analysis that needs one
(e.g. SonarQube Cloud) does not run there; a maintainer runs it before merging. Run the quality gates below
locally so the pull request arrives clean.
locally so the pull request arrives clean. The CI run of a first-time contributor's fork waits for a maintainer's
approval before it starts.
- Maintainers may push to your branch ("Allow edits by maintainers") or carry the change on in a branch of their
own; you stay credited as co-author of the squashed commit.

Expand Down
Loading