diff --git a/.claude/agents/squad-devils-advocate.md b/.claude/agents/squad-devils-advocate.md index 20f7e5e4..129837c2 100644 --- a/.claude/agents/squad-devils-advocate.md +++ b/.claude/agents/squad-devils-advocate.md @@ -40,6 +40,9 @@ Your job is to find what the plan got wrong **before** it is built, not to revie with the same defect, a missed caller, a documented guarantee the plan does not mention). - **Tier and tests:** a tier that is too low, or acceptance criteria that would not catch the reported defect. +- **Target host:** a plan for an installer or deployment (or a document for an operator) that does not match + the list of what the target host has and may have in `spec.md`, or a spec for one that has no such list, is a + `major` objection (*Operator environments and documented procedures* in `.squad/routing.md`). - **Sample:** a plan for a parser, decoder or reader of an external format that names no real sample of it (under the test data layout or on the issue) is built from a description and is a `major` objection. diff --git a/.claude/agents/squad-lead.md b/.claude/agents/squad-lead.md index 11391142..5f00d1e8 100644 --- a/.claude/agents/squad-lead.md +++ b/.claude/agents/squad-lead.md @@ -31,7 +31,9 @@ The orchestrator tells you which **mode** to run: the behavior. Size the spec to the feature: one scenario per behavior a user can tell apart, only the edge cases that change what the user sees, and a link instead of a restatement of the issue, an area document or a record — a small feature gets a short spec. Check every claim the issue makes about today's behavior against - the code before you build a scenario on it. A feature that already exists or contradicts an accepted decision record is + the code before you build a scenario on it. For an installer or deployment feature, ask the Product Manager + what the target host has and may have (`RESULT: ESCALATE`, *Operator environments and documented procedures* + in `.squad/routing.md`) and record the answers as scenarios and criteria. A feature that already exists or contradicts an accepted decision record is `RESULT: NO CHANGE` with the reason and a proposed issue comment. - `plan` — pick the tier on the change (`.squad/routing.md`, *Tiers*). For tiers `docs` and `patch` write **no** `plan.md`: return the tier with its justification, the files and the exact edits, and the acceptance diff --git a/.claude/agents/squad-tester.md b/.claude/agents/squad-tester.md index 199fbd6a..d42ab4c4 100644 --- a/.claude/agents/squad-tester.md +++ b/.claude/agents/squad-tester.md @@ -30,6 +30,10 @@ Mode `tests-first`: base input), never as an absolute duration; the limit is at least three times the highest ratio you measured, with the instrumentation of *Test with coverage* included (`docs/UNIT_TESTS.md`). A case about an escape keeps its backslash on disk and asserts it (*Escapes in files* in `.squad/team.md`). + For a documented shell procedure derive the cases from the plan's list of what happens when the lines are + pasted at once or one at a time, one fails or a variable is lost; a test that reads a repository document + normalizes `\r\n` (`docs/UNIT_TESTS.md`); for a script only CI can run, run the local smoke substitute + (*Writing tests* in `.squad/stack.md`) before you hand back. For a feature, the criteria are `spec.md`'s and their tests the plan's *Tests per criterion*: every field and value a criterion names, and the scenarios and edge cases it lists, are asserted by a test; name one no test asserts in your report. @@ -78,5 +82,9 @@ Rules: detects the failure, and measure before handing over. - Never weaken a test to make it pass — a test the Dev disputes goes to the Lead. Never write production code (also not in a scratch copy; the one exception is the throwaway mutation of mode `coverage`). +- A file that cannot run in the session (a script only CI can run, a generated file) is edited only with exact, + unique string replacement (the Edit tool), never by slicing the file on a search marker: a marker can match + more than one place, and a rebuilt file keeps the duplicate. After the edit, compare `git diff --stat` with the + intended size and list the case or function headers (`grep -n`) to show each appears once. Report: tests added (names), their result, the coverage output. diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 78ad5af6..2d7d614a 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -28,9 +28,9 @@ if ! command -v reihitsu-format >/dev/null 2>&1; then dotnet tool install -g Reihitsu.Cli fi -# Scanner of the *Sonar gate* in .squad/stack.md; the gate reports NOT CHECKED when it is missing, so a failed install does not fail the session. -if ! command -v dotnet-sonarscanner >/dev/null 2>&1; then - dotnet tool install -g dotnet-sonarscanner || echo "dotnet-sonarscanner could not be installed; the Sonar gate will report NOT CHECKED" >&2 +# Local tools of the repository's manifest (dotnet-sonarscanner for the *Sonar gate*, a formatter 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 fi # Restore every solution at the repository root (the squad's settings name the one the gates build). diff --git a/.claude/skills/create-pr/SKILL.md b/.claude/skills/create-pr/SKILL.md index ae188009..1d45b13c 100644 --- a/.claude/skills/create-pr/SKILL.md +++ b/.claude/skills/create-pr/SKILL.md @@ -63,6 +63,11 @@ repository. verified) — including the unit-test, formatting, analyzer, coverage, documentation and dependency items, not just the general ones - wrap the body in a HEREDOC so the formatting survives + - set one label on the PR (`issue_write`, method `update`, `labels`) for the release notes + (`.github/release.yml`): `breaking-change` for a change that breaks existing users, else `enhancement` for a + feature, `security` for a security fix, `bug` for a bug fix (with a linked issue: the matching label of the + issue, else judge from the change); none for anything else (it lands under *Other + changes*), `ignore-for-release` for a change that is not worth a release note - read the body back right after creating or updating the PR (`pull_request_read`, method `get`) and compare its links and code spans with what you sent; a mangled one is fixed with `update_pull_request` diff --git a/.claude/skills/squad-issue/SKILL.md b/.claude/skills/squad-issue/SKILL.md index 37b0bd38..c619a94a 100644 --- a/.claude/skills/squad-issue/SKILL.md +++ b/.claude/skills/squad-issue/SKILL.md @@ -69,7 +69,10 @@ every Git and GitHub action yourself, including follow-up issues the Lead decide agent is not the member's last fresh launch) on the call that carries its numbers — the amend of its pending row — with the notification's numbers unchanged (only the tokens are cumulative). Name a fix, delta or revise round as such in its step cell; a continuation at another step than its fresh launch draws a warning (a new mode is a fresh launch). - - Rows of the Orchestrator, the Product Manager and the User take no metric option. + - Rows of the Orchestrator, the Product Manager and the User take no metric option; their member cells are + exactly `Orchestrator`, `Product Manager` and `User` (`squad-orchestrator` is written as `Orchestrator`). + - `--continued-from` is only for continuing the same agent; a fresh launch of the same role at another step + is logged without it. - After step 10 every call — row, amend, summary — needs `--stdin` (step 10); without it, it fails. - Everything else, and what each refusal means: `python3 .squad/tools/squad-log.py --help`. diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..267cd41d --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-sonarscanner": { + "version": "12.0.0", + "commands": [ + "dotnet-sonarscanner" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..f47ee90f --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,27 @@ +# Controls the notes GitHub generates for a release ("Generate release notes" on the release page). +# GitHub groups by pull request label only (not by title); the squad's create-pr sets the label. +# Nothing else is excluded: a pull request without a known label (Dependabot's included) is listed under +# "Other changes". +# A pull request lands in the first category whose labels it carries, so the order matters. +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: "Breaking changes" + labels: + - breaking-change + - title: "Features" + labels: + - enhancement + - feature + - title: "Security fixes" + labels: + - security + - title: "Bug fixes" + labels: + - bug + - title: "Other changes" + labels: + - "*" diff --git a/.squad/orchestration.md b/.squad/orchestration.md index 22278313..b85e9bfd 100644 --- a/.squad/orchestration.md +++ b/.squad/orchestration.md @@ -109,7 +109,7 @@ review (`update_pull_request` with `draft: false`) only when all of this holds o waited for, not counted as passed); - `python3 .squad/tools/sonar-check.py pr ` exits 0: the CI code analysis of **this** head passed its quality gate with **no new issue** and no hotspot to review (a passed gate with new issues is still - rework). PENDING (exit 3): wait and run it again, never read a `Quality Gate passed` comment instead; still + rework). PENDING (exit 3) means a newer analysis than the shown one is still to come, not a problem of the head (the analysis can lag the pushes by several commits): wait and run it again, never read a `Quality Gate passed` comment instead; still PENDING once every CI check of the head has finished means the analysis cannot be read; - no commit for a known failure or finding is still to be pushed, and after a fix in the squad skills the re-approval of step 9 has passed on this head. diff --git a/.squad/routing.md b/.squad/routing.md index f011f11b..f3bd4ef8 100644 --- a/.squad/routing.md +++ b/.squad/routing.md @@ -128,6 +128,30 @@ plan names it as the criterion's verification with the Dev as its owner. A build working tree does not count: it restores with every source present. A red CI on such a criterion after that check is worked in step 11. +### Operator environments and documented procedures + +A criterion or document that depends on the operator's environment (network, DNS, access policy, console, +upload tools, what the target host has installed) is planned against that environment, not against the +session's: + +- **Target host (install and deploy features).** The spec asks the Product Manager what the target host has + and may have (tools such as `git`, network access to which hosts, DNS, the console it is used from, the + order of steps the operator will paste) and records the answers as scenarios and acceptance criteria. The + Devil's Advocate checks the plan against that list. +- **Dry run.** The plan schedules a dry run of the documented steps by the Product Manager (or a stated + stand-in) **before** the documentation is reviewed, and lists the hosts and software versions the run needs + (for example the backend version with the API the procedure calls). Every statement of the document that no + run has shown is marked as such in the document. When an external page cannot be reached from the session, + the plan asks the Product Manager to confirm those statements in the earliest real run, not at the end. +- **Shell procedures.** For a documented shell procedure the plan states, before Security's plan review, what + happens when (a) all lines are pasted at once, (b) one line at a time, (c) one line fails, (d) a variable + is lost, and names the tests (unit and CI) that hold each answer; the Tester derives its cases from that + list. A line that continues after a failed step, so that a half-finished state can still be run, is a + defect of the design. +- **CI-only rows.** A script only CI can run (it needs systemd, root or a service the session lacks) has a + local smoke substitute (*Writing tests* in `.squad/stack.md`); the plan lists the rows that can run in CI + only, and the Tester runs the substitute before handing back. + ### Code a unit test cannot cover A file of the change that no unit test can cover — a browser script the stack has no test runner for, a diff --git a/.squad/stack.md b/.squad/stack.md index cd978e29..18cf32b6 100644 --- a/.squad/stack.md +++ b/.squad/stack.md @@ -92,12 +92,13 @@ untracked files other than the coverage reports) and needs a SonarQube credentia else `SONAR_TOKEN` in the environment, which every process of the session can read. The scanner also needs network access to its download hosts (`api.sonarcloud.io` and `scanner.sonarcloud.io`, which the secret's hosts cover, and the package registry); without them it reports NOT CHECKED, which on a branch without a pull request yet is not final: step 11 reads the draft pull -request with `python3 .squad/tools/sonar-check.py pr ` instead, and that result counts. The SessionStart hook installs -`dotnet-sonarscanner` and puts `~/.dotnet/tools` on the `PATH`; a failed `sonar-check.py` read names the request and +request with `python3 .squad/tools/sonar-check.py pr ` instead, and that result counts. The SessionStart hook restores +`dotnet-sonarscanner` from the tool manifest and puts `~/.dotnet/tools` on the `PATH`; a failed `sonar-check.py` read names the request and the response body (HTTP status included), so the cause of an HTTP error can be told from the output. A finding of a rule no local analyzer reports is noted in this section, in the same change, instead of a squad lesson. -The .NET scanner is the `dotnet-sonarscanner` tool, installed globally with `dotnet tool install -g dotnet-sonarscanner` -(a local install writes an untracked `dotnet-tools.json` to the repository root, which the gate refuses); its `begin` +The .NET scanner is the `dotnet-sonarscanner` tool, pinned in the tool manifest `.config/dotnet-tools.json`, which +`dotnet tool restore` must have restored before the gate in a fresh cloud session: the SessionStart hook does it, run it +by hand where the hook did not; its `begin` arguments mirror the CI step, plus the branch name and the repository root as base directory, and it needs the full, non-incremental build between `begin` and `end`. The local coverage report is Cobertura, which the scanner does not read, so the branch analysis carries no coverage figure: the *Coverage @@ -172,6 +173,12 @@ the variable name, for example) whose text a test pins, never a suppression. ## Writing tests +**Scripts only CI can run.** A script that needs systemd, root or a service the session lacks (an +end-to-end install check, for example) has a local smoke substitute, which the Tester runs before handing back: +`bash -n` and `shellcheck` on the script, and a mode of the script (or a test) that runs the documented lines +against a scratch directory with stub commands, without systemd. The plan lists the rows that can only run in +CI (`.squad/routing.md`, *Operator environments and documented procedures*). + See `docs/UNIT_TESTS.md` — MSTest with **NSubstitute** for collaborators that cross an infrastructure boundary, EF Core InMemory / SQLite for data tests. The rules the MSTest and Sonar analyzers enforce and the Code Officer cannot fix without handing back: pass `TestContext.CancellationToken` to every call that accepts a token diff --git a/.squad/template.json b/.squad/template.json index 027bbb66..367d63c6 100644 --- a/.squad/template.json +++ b/.squad/template.json @@ -1,6 +1,6 @@ { "repository": "LarsLaskowski/Squad-Spec-Repository-Template", - "commit": "cf3cc08ad9f7a54b7f33c9509625af22c57a7a54", + "commit": "b38a99f77efcbab02f0e1c04c55f301682789111", "profile": "dotnet", "profiles": [ "dotnet" diff --git a/.squad/tools/sonar-check.py b/.squad/tools/sonar-check.py index 9c1e3451..cb7fe4dd 100644 --- a/.squad/tools/sonar-check.py +++ b/.squad/tools/sonar-check.py @@ -26,6 +26,11 @@ server from `SONAR_HOST` (default https://sonarcloud.io); the scanner's report file from `SONAR_REPORT_TASK` (default `.scannerwork/report-task.txt`). +Two reads have a second endpoint for a credential that may browse the project but not use the first one (a +403 from the proxy-authenticated session): the pull request's commit comes from `project_analyses/search` when +`project_pull_requests/list` is refused, and the scan's processing status from `ce/activity` when `ce/task` is +refused. + A read that SonarQube answers with an HTTP error is NOT CHECKED with the request (path and parameters, never the token) and the response body, so a rejected parameter can be told from a missing project or the proxy. @@ -77,6 +82,10 @@ class NotChecked(Exception): class ApiError(OSError): """SonarQube answered a request with an HTTP error; the message names the request and the response.""" + def __init__(self, message, status=None): + super().__init__(message) + self.status = status + class Usage(argparse.ArgumentParser): def error(self, message): @@ -118,7 +127,7 @@ def api(path, params, token=None): except urllib.error.HTTPError as error: # The URL carries the parameters but never the token (it travels in a header), so it may be printed. body = error.read().decode("utf-8", "replace").strip()[:ERROR_BODY_CHARS] - raise ApiError(f"HTTP {error.code} for GET {url}: {body or 'empty response body'}") from error + raise ApiError(f"HTTP {error.code} for GET {url}: {body or 'empty response body'}", error.code) from error def proxy_credential(): @@ -178,15 +187,27 @@ def report(findings, gate=None): return CLEAN +def analysed_commit(key, number, token=None): + """The commit SonarQube analysed last for the pull request, or "" when there is no analysis.""" + try: + analyses = api("project_pull_requests/list", {"project": key}, token).get("pullRequests", []) + except ApiError as error: + if error.status != 403: + raise + found = api("project_analyses/search", {"project": key, "pullRequest": number, "ps": 1}, token) + return next((a.get("revision", "") for a in found.get("analyses", [])), "") + analysis = next((a for a in analyses if a.get("key") == str(number)), None) + return (analysis or {}).get("commit", {}).get("sha", "") + + def check_pr(key, number, head, token=None): """The analysis of the pull request, matched to the head.""" - analyses = api("project_pull_requests/list", {"project": key}, token).get("pullRequests", []) - analysis = next((a for a in analyses if a.get("key") == str(number)), None) - analysed = (analysis or {}).get("commit", {}).get("sha", "") - if not analysis or len(head) < 7 or not analysed.startswith(head): # a full SHA, or an abbreviation of it + analysed = analysed_commit(key, number, token) + if len(head) < 7 or not analysed or len(head) < 7 or not analysed.startswith(head): # a full SHA, or an abbreviation of it print(f"PENDING: SonarQube has no analysis of pull request {number} at {head[:12]} yet" - + (f" (latest: {analysed[:12]})" if analysed else "") + "; wait and run again. Still PENDING after " - "every CI check of this head finished: the analysis cannot be read, and the PR stays a draft.") + + (f" (latest: {analysed[:12]})" if analysed else "") + ". PENDING says that a newer analysis than " + "the one shown is still to come, not that the head has a problem: wait and run again. Still PENDING " + "after every CI check of this head finished: the analysis cannot be read, and the PR stays a draft.") return PENDING params = {"projectKey": key, "pullRequest": number} gate = api("qualitygates/project_status", params, token)["projectStatus"]["status"] @@ -210,11 +231,25 @@ def ce_task_id(): return None +def ce_task(task, token): + """The compute engine task; `ce/activity` answers for a credential that `ce/task` refuses.""" + try: + return api("ce/task", {"id": task}, token)["task"] + except ApiError as error: + if error.status != 403: + raise + key = project_key() + for candidate in api("ce/activity", {"component": key, "ps": 20}, token).get("tasks", []): + if candidate.get("id") == task: + return candidate + return {"id": task, "status": "PENDING"} + + def wait_for_task(task, token): """Wait until SonarQube has processed the uploaded report; return the task (its status says how it ended).""" deadline = time.monotonic() + WAIT_SECONDS while True: - result = api("ce/task", {"id": task}, token)["task"] + result = ce_task(task, token) if result["status"] not in ("PENDING", "IN_PROGRESS"): return result if time.monotonic() >= deadline: diff --git a/.squad/tools/squad-log.py b/.squad/tools/squad-log.py index 60e6cc47..dedffa3a 100644 --- a/.squad/tools/squad-log.py +++ b/.squad/tools/squad-log.py @@ -35,7 +35,8 @@ fresh launch of that step. A continuation logged at another step number than the fresh launch it continues prints a warning, unless its step cell names a fix, delta or revise round: the first launch of another mode (`skeleton` to `implement`, `tests-first` to `coverage`, `approve-pr`, `decide`) is a fresh launch, not a - continuation; + continuation. `--continued` and `--continued-from` are only for continuing the same agent: a fresh launch of + the same role at another step is logged without them; - the usage block often arrives only after the member's hand-back: `--amend-last MEMBER` with the same options adds the trailer to the last row of that member afterwards, so the row can be written as soon as the report is in. A row gets its trailer once (a pending one is completed); a second amend is refused. When a @@ -52,9 +53,10 @@ launch, name it as unreported and exit 0, so the table is posted as the tool prints it instead of being summed by hand or left out. `--replace-last` with the real numbers turns it into a full trailer later; - rows of the Orchestrator, the Product Manager and the User (an answer, a measurement the user ran) are - not launches: write them with that member cell and no metric option, the summary does not ask for usage; - a metric option, `--amend-last`, `--replace-last` or `--unreported` for such a member is refused, since no - task notification exists for it; + not launches: write them with exactly the member cell `Orchestrator`, `Product Manager` or `User` (the + spellings `squad-orchestrator` and `squad-user` are written as those cells) and no metric option, the + summary does not ask for usage; a metric option, `--amend-last`, `--replace-last` or `--unreported` for such + a member is refused, since no task notification exists for it; - the metrics are what a launch really cost: fewer than 1,000 tokens (every launch loads its agent file and instructions first, so `--tokens 0` or `--tokens 1` can only be a placeholder) and negative numbers are refused, so a placeholder cannot be logged by accident. A row for a launch whose notification has not @@ -130,6 +132,7 @@ TWO_MEMBERS = re.compile(r"[,/+&]|\band\b") ORCHESTRATOR = "orchestrator" NOT_LAUNCHES = {ORCHESTRATOR, "product manager", "user"} +NOT_LAUNCH_CELLS = {"orchestrator": "Orchestrator", "product manager": "Product Manager", "user": "User"} STAMP = r"\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?" ROW = re.compile(rf"^\| (?P{STAMP}) \| (?P(?:[^|\\]|\\\|)*) \| (?P(?:[^|\\]|\\\|)*) \| " r"(?P.*) \|\s*$") @@ -148,6 +151,15 @@ def escape_controls(text): for ch in text) +def canonical_member(member): + """`Orchestrator`, `Product Manager` and `User` for their `squad-` prefixed or differently cased spellings + (`squad-orchestrator`, `squad-user`); any other member is returned as given.""" + if member is None: + return None + name = re.sub(r"^squad[- ]", "", member.strip().lower()).replace("-", " ") + return NOT_LAUNCH_CELLS.get(name, member) + + def cell(text): text = escape_controls(text).replace("\t", " ") text = re.sub(r"\r\n|\r|\n", "
", text.strip()) @@ -738,6 +750,9 @@ def run_on_stdin(parser, args, root): def main(): parser = build_parser() args = parser.parse_args() + args.member = canonical_member(args.member) + args.amend_last = canonical_member(args.amend_last) + args.replace_last = canonical_member(args.replace_last) toplevel = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=False) root = toplevel.stdout.strip() if toplevel.returncode == 0 else os.getcwd() if args.stdin: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 40abed99..b0e65f64 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -82,6 +82,9 @@ feature. Use before/after screenshots in the PR description when a change affect When a PR is related to an issue, use the `Closes #issuenumber` syntax so the issue links to the 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. + Follow the PR template in [`.github/pull_request_template.md`](../.github/pull_request_template.md). ## Contributions from outside diff --git a/docs/UNIT_TESTS.md b/docs/UNIT_TESTS.md index 49c3560c..7459aedb 100644 --- a/docs/UNIT_TESTS.md +++ b/docs/UNIT_TESTS.md @@ -303,6 +303,13 @@ public void UpdateDetectionServiceDigestChangeReturnsCurrentTagUpdate() } ``` +## Tests that read repository documents + +`.gitattributes` may check files out with CRLF (`*.md text eol=crlf`), so on a CI runner a document carries +`\r\n` where the repository blob has `\n`. A test or script that reads a repository document (`*.md`, and the +other files `.gitattributes` checks out with CRLF) normalizes `\r\n` to `\n` before it compares lines or feeds +them to a shell. The first such test also tests the normalizer with CRLF input. + ## Code coverage Code coverage is collected with `coverlet.collector`, already referenced by diff --git a/specs/_template/plan.md b/specs/_template/plan.md index 12f61119..c5ee9546 100644 --- a/specs/_template/plan.md +++ b/specs/_template/plan.md @@ -45,6 +45,12 @@ first, named strictly by *Layout* in `.squad/stack.md`. A criterion verified wit | -- | ------- | --------- | | AC1 | … | … | +A test or script that reads a repository document (`*.md` and the other files `.gitattributes` checks out with +CRLF) normalizes `\r\n` to `\n` first; the first such test also tests the normalizer with CRLF input +(`docs/UNIT_TESTS.md`). For a documented shell procedure, a list under this table names the test for each case +of *Operator environments and documented procedures* (`.squad/routing.md`), and the rows that can only run in +CI. + ## Verification without tests Only when the change touches no production or test code, in any tier: steps 4, 5 and the *Coverage gate* diff --git a/specs/_template/spec.md b/specs/_template/spec.md index a98ab475..d25bbf8e 100644 --- a/specs/_template/spec.md +++ b/specs/_template/spec.md @@ -29,7 +29,10 @@ repeated or concurrent action — numbered, each with what the user sees (the ex ## Non-functional requirements +and configuration), new configuration with its default, logs and metrics — one bullet each, or "none". For an +installer or deployment feature also what the target host has and may have (tools, network access, DNS, the +console, the order of steps the operator pastes), as the Product Manager answered (*Operator environments and +documented procedures* in `.squad/routing.md`). --> ## Acceptance criteria