diff --git a/docs/design/phase-3-backlog.issues.json b/docs/design/phase-3-backlog.issues.json new file mode 100644 index 0000000..615e34d --- /dev/null +++ b/docs/design/phase-3-backlog.issues.json @@ -0,0 +1,40 @@ +{ + "US-20": 313, + "US-21": 314, + "US-22": 315, + "US-23": 316, + "US-24": 317, + "US-25": 318, + "US-26": 319, + "US-27": 320, + "US-28": 78, + "US-29": 321, + "US-30": 322, + "US-31": 323, + "US-32": 324, + "US-33": 325, + "US-34": 326, + "US-35": 327, + "US-36": 328, + "US-37": 329, + "US-38": 330, + "US-39": 331, + "US-40": 332, + "US-41": 333, + "US-42": 334, + "US-43": 335, + "US-44": 336, + "US-45": 337, + "US-46": 338, + "US-47": 339, + "US-48": 340, + "US-49": 341, + "US-50": 342, + "US-51": 343, + "US-52": 344, + "US-53": 220, + "US-54": 345, + "US-55": 346, + "US-56": 347, + "US-57": 308 +} diff --git a/docs/design/phase-3-backlog.md b/docs/design/phase-3-backlog.md new file mode 100644 index 0000000..fa61014 --- /dev/null +++ b/docs/design/phase-3-backlog.md @@ -0,0 +1,213 @@ + + +# Phase 3 — market readiness + +*Everything between Netwise today and a product that could be sold.* + +Phase 0 was consolidation. Phase 1 was the four capabilities. Phase 2 was direction only. This is the first backlog written against a different question: not *what would make this a better capstone* but **what would stop a stranger who paid for it from asking for a refund**. + +--- + +## Where these came from + +| Range | Source | +|---|---| +| `US-20` – `US-39` | **Discussion #296**, unchanged. Twenty stories designed and argued in early September and never turned into work. Their acceptance criteria are copied rather than rewritten. | +| `US-40` – `US-56` | **New.** Each one closes a gap a paying customer hits in the first week, or one a buyer's security review asks about and we currently cannot answer. | + +Three stories already existed as issues and are folded in rather than created again — creating them twice would be this project's own recurring defect, committed by the backlog written to avoid it: + +| Story | Existing issue | +|---|---| +| `US-28` | [#78](https://github.com/ARSH871-bot/Netwise/issues/78) — Convert my whole pfSense export, including NAT | +| `US-53` | [#220](https://github.com/ARSH871-bot/Netwise/issues/220) — One real compliance benchmark pack (CIS Cisco IOS) | +| `US-57` | [#308](https://github.com/ARSH871-bot/Netwise/issues/308) — Coverage on every surface that shows findings, or none | + +Nothing here is a feature because it sounds impressive. Every story names the thing that is currently untrue, unmeasured or unreachable. + +--- + +## The distribution, stated rather than arranged + +**38 stories. Arsh has 28. Ankeet has 4, Shubham has 3, Samika has 3.** + +That is deliberate, and it is a decision rather than an accident, so it is written down where the people affected by it can read it. + +**Why.** The remaining calendar is short and most of this milestone is sequential: US-22 waits on US-33, US-45 waits on US-35, US-46 waits on US-22 and US-44. A dependency chain moves at the speed of whoever is slowest to pick up their link. Concentrating the chain on one person removes the hand-off latency, which is the largest single cost in a four-person team working part-time around other papers. + +**What each of the other three gets** is a short, self-contained slice inside the vertical they already own, with no story in it that blocks anybody else. They can finish, stop, and not be the reason something else is waiting. + +**What it costs, which is the part worth saying out loud.** #249 measured that Arsh authored 61% of pull requests and consumed 62% of the team's review capacity, and — more importantly — that **review, not authorship, is where this project's defects have actually been caught**. The fixture corruption in #304, the student IDs in #311, the stale amendment table, the false claim in the portfolio: every one was found by somebody reading somebody else's work. + +So this split does not reduce the team's work. It moves it from authoring to reviewing. **Every story here still needs a current approving review from someone who did not write it (M-1), and new work still voids an approval (M-2 rule 1).** If the other three treat a light authoring load as a light total load, this milestone will ship faster and be worse, and the failure will not be visible until something built on an unreviewed foundation has to be unpicked. + +The honest summary: **three people are being asked to write less and read more.** That is the trade, and it is only a good trade if the reading actually happens. + +--- + +## The backlog + +### Honesty + +*The product's central claim is that it never says “clean” when it means “unchecked”. These are the places that claim is still only half-kept.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-23` | Make `access_control` read my policy | [#316](https://github.com/ARSH871-bot/Netwise/issues/316) | M | Arsh | +| `US-24` | Show me what Netwise could not read | [#317](https://github.com/ARSH871-bot/Netwise/issues/317) | M | Arsh | +| `US-26` | Make `routing` read my policy | [#319](https://github.com/ARSH871-bot/Netwise/issues/319) | M | Arsh | +| `US-40` | Prove that nothing left my machine | [#332](https://github.com/ARSH871-bot/Netwise/issues/332) | M | Arsh | +| `US-57` | Coverage on every surface that shows findings, or none *(already open)* | [#308](https://github.com/ARSH871-bot/Netwise/issues/308) | M | Arsh | + +### Distribution + +*Nobody can buy something they cannot install.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-41` | Install Netwise with one command | [#333](https://github.com/ARSH871-bot/Netwise/issues/333) | M | Arsh | +| `US-42` | Upgrade without losing my data | [#334](https://github.com/ARSH871-bot/Netwise/issues/334) | M | Arsh | +| `US-49` | Install it with no internet at all | [#341](https://github.com/ARSH871-bot/Netwise/issues/341) | M | Arsh | + +### Scale + +*Speed was measured in #218 and is not the problem: fifty devices analyse in 6.9 seconds. What `docs/scale.md` found instead is that the analysis does not GROW with the network -- fifty devices in, one device examined. These stories are about that.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-21` | Analyse my whole network at once, not device by device | [#314](https://github.com/ARSH871-bot/Netwise/issues/314) | M | Arsh | +| `US-43` | Tell me how big a network you can actually handle | [#335](https://github.com/ARSH871-bot/Netwise/issues/335) | L | Arsh | +| `US-44` | Run a long scan in the background | [#336](https://github.com/ARSH871-bot/Netwise/issues/336) | L | Arsh | + +### Multi-tenant + +*One person on one laptop is a tool. Several people in one company is a product.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-35` | Sign in, so my scans are mine | [#327](https://github.com/ARSH871-bot/Netwise/issues/327) | L | Samika | +| `US-45` | Give my team shared access, with roles | [#337](https://github.com/ARSH871-bot/Netwise/issues/337) | L | Arsh | +| `US-52` | Tell me what you keep, and delete it when I say | [#344](https://github.com/ARSH871-bot/Netwise/issues/344) | M | Arsh | + +### Automation + +*A scan somebody has to remember to run is a scan that stops happening in week three.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-20` | Check my configs in CI, before the change merges | [#313](https://github.com/ARSH871-bot/Netwise/issues/313) | M | Arsh | +| `US-46` | Scan on a schedule, without me starting it | [#338](https://github.com/ARSH871-bot/Netwise/issues/338) | M | Arsh | +| `US-47` | Read my configs from where they already are | [#339](https://github.com/ARSH871-bot/Netwise/issues/339) | M | Arsh | +| `US-48` | Send findings to the tools my team already uses | [#340](https://github.com/ARSH871-bot/Netwise/issues/340) | M | Arsh | + +### Content + +*Our rules are ours. A buyer wants a standard's rules, and a way into writing their own.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-34` | Draft a policy from a config I already have | [#326](https://github.com/ARSH871-bot/Netwise/issues/326) | M | Arsh | +| `US-53` | One real compliance benchmark pack (CIS Cisco IOS) *(already open)* | [#220](https://github.com/ARSH871-bot/Netwise/issues/220) | L | Arsh | + +### Remediation + +*Finding the problem is half the value. The other half is the line of config that fixes it.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-54` | Give me the fix, not just the problem | [#345](https://github.com/ARSH871-bot/Netwise/issues/345) | L | Arsh | + +### Vendors + +*Four vendor fixtures exist and three of them -- Arista, Juniper and NX-OS -- already produce a real finding end to end. What is left is the fourth, and saying so anywhere a user can see it.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-27` | Analyse a vendor that is not Cisco, end to end | [#320](https://github.com/ARSH871-bot/Netwise/issues/320) | L | Ankeet | +| `US-28` | Convert my whole pfSense export, including NAT *(already open)* | [#78](https://github.com/ARSH871-bot/Netwise/issues/78) | L | Ankeet | + +### AI + +*The layer with the most attention on it and the least measurement behind it.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-25` | Let me ask a follow-up | [#318](https://github.com/ARSH871-bot/Netwise/issues/318) | L | Ankeet | +| `US-55` | Show me the explanation layer is any good | [#346](https://github.com/ARSH871-bot/Netwise/issues/346) | M | Ankeet | + +### Policy + +*The user's own rules are a first-class artefact and deserve the same scrutiny we give their configs.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-30` | Tell me if my own policy contradicts itself | [#322](https://github.com/ARSH871-bot/Netwise/issues/322) | M | Shubham | +| `US-31` | Check a proposed change against my whole policy | [#323](https://github.com/ARSH871-bot/Netwise/issues/323) | M | Shubham | +| `US-32` | Tell me which of my policy rules never fire | [#324](https://github.com/ARSH871-bot/Netwise/issues/324) | S | Shubham | +| `US-33` | Recognise the same problem across two scans | [#325](https://github.com/ARSH871-bot/Netwise/issues/325) | M | Arsh | + +### UX + +*Most of these are cheap. Together they are the difference between a demo and a thing someone uses on a Tuesday.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-22` | Tell me only what changed since the last scan | [#315](https://github.com/ARSH871-bot/Netwise/issues/315) | M | Arsh | +| `US-36` | Rank by what it would cost me, not just what kind of bug it is | [#328](https://github.com/ARSH871-bot/Netwise/issues/328) | M | Arsh | +| `US-37` | Give me something I can hand to an auditor | [#329](https://github.com/ARSH871-bot/Netwise/issues/329) | M | Arsh | +| `US-38` | Let me use it with a keyboard and a screen reader | [#330](https://github.com/ARSH871-bot/Netwise/issues/330) | M | Samika | +| `US-39` | Show me progress during a long scan | [#331](https://github.com/ARSH871-bot/Netwise/issues/331) | S | Samika | +| `US-56` | Make my first five minutes work | [#347](https://github.com/ARSH871-bot/Netwise/issues/347) | S | Arsh | + +### Trust + +*The questions a buyer's security and procurement teams ask before they ask anything about features.* + +| | Story | Issue | Size | Owner | +|---|---|---|---|---| +| `US-29` | Explain related findings as one story | [#321](https://github.com/ARSH871-bot/Netwise/issues/321) | M | Arsh | +| `US-50` | Give me an SBOM and a stated dependency policy | [#342](https://github.com/ARSH871-bot/Netwise/issues/342) | S | Arsh | +| `US-51` | Point Netwise at its own deployment | [#343](https://github.com/ARSH871-bot/Netwise/issues/343) | S | Arsh | + +--- + +## Order of work + +Not a schedule. A dependency order, so nothing is started before the thing it needs exists. + +``` +1 install and honesty US-41 US-40 US-56 US-23 US-26 US-24 + US-57 + a stranger can run it, and it tells the truth about what it read + +2 identity and persistence US-35 US-42 US-33 US-22 + results stop being disposable, and a finding keeps its name + +3 scale and automation US-21 US-43 US-44 US-20 US-46 US-47 + it survives a real estate and runs without being asked + +4 breadth and content US-27 US-28 US-53 US-34 US-54 US-31 + more vendors, real benchmarks, and the fix as well as the fault + +5 the buyer's questions US-50 US-51 US-52 US-45 US-37 US-48 + US-49 + procurement, security review, and the team-sized product + +throughout US-25 US-29 US-30 US-32 US-36 + US-38 US-39 US-55 + independent of the chain; can start any time +``` + +--- + +## What would make this milestone a failure + +Worth writing down now, while it is cheap to say. + +1. **A story marked done that widened a claim rather than a capability.** US-24, US-40 and US-43 are all one careless commit away from asserting more coverage than they deliver. That is the exact failure this product exists to catch in other people's networks. +2. **Reviews thinning as authoring concentrates.** See the section above. This is the most likely way it goes wrong. +3. **This document going stale.** It is generated from a single source along with the issues themselves, precisely so it cannot disagree with them. If it is ever hand-edited, that guarantee is gone and it becomes one more copy of a fact — which is the defect family this project has been recording since the first sprint. + diff --git a/tests/test_phase3_backlog_doc.py b/tests/test_phase3_backlog_doc.py new file mode 100644 index 0000000..f8d2caf --- /dev/null +++ b/tests/test_phase3_backlog_doc.py @@ -0,0 +1,100 @@ +"""`docs/design/phase-3-backlog.md` must be what the generator produces. + +WHY THIS FILE EXISTS + The document says, in its own last section, that it cannot disagree with + the GitHub issues because both are generated from one source. That claim + is only true while nobody hand-edits the document. Nothing enforced it, + which made it exactly the kind of sentence this project keeps writing and + then falsifying -- a described practice standing in for a performed one. + + So the claim is checked instead of asserted. + +WHY THIS IS NOT #267 + #267 is the reason to be careful here. `docs/traceability.md` carries + per-file test counts, so a CI check on it would be red every time anybody + adds a test -- permanently red, and therefore ignored. + + This document carries no count that `main` can move. Its content is a + function of the story list and the recorded issue numbers, both of which + live in files in this repository. It changes when somebody edits the + generator, and at no other time. That is what makes the check stable + rather than noise. + +WHAT FAILURE MEANS + Either the document was edited by hand -- regenerate it and put the change + in the generator instead -- or the generator changed and the document was + not regenerated. Both are the same defect and both have the same fix: + + python -m tools.make_phase3_backlog doc + +These need neither Batfish nor Ollama. +""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +GENERATOR = REPO_ROOT / "tools" / "make_phase3_backlog.py" +DOCUMENT = REPO_ROOT / "docs" / "design" / "phase-3-backlog.md" + + +def _generator(): + """Import the script by path -- tools/ is not a package on sys.path.""" + spec = importlib.util.spec_from_file_location("_phase3", GENERATOR) + module = importlib.util.module_from_spec(spec) + sys.modules["_phase3"] = module + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def gen(): + if not GENERATOR.exists(): # pragma: no cover - the file is tracked + pytest.skip("generator not present") + return _generator() + + +def test_document_matches_the_generator(gen): + """The committed document is byte-identical to a fresh generation.""" + on_disk = DOCUMENT.read_text(encoding="utf-8") + assert on_disk == gen.document(), ( + "docs/design/phase-3-backlog.md is not what the generator produces. " + "Run: python -m tools.make_phase3_backlog doc" + ) + + +def test_every_story_has_an_issue_number(gen): + """A backlog row with no issue behind it is a plan, not work.""" + import json + + numbers = json.loads(gen.NUMBERS.read_text(encoding="utf-8")) + every = {row[0] for row in gen.STORIES} | {e[0] for e in gen.EXISTING} + assert set(numbers) == every, ( + f"stories with no issue: {sorted(every - set(numbers))}; " + f"issues with no story: {sorted(set(numbers) - every)}" + ) + + +def test_story_ids_are_unique(gen): + """Two stories sharing an id is the duplicate-`id` bug in a backlog.""" + ids = [row[0] for row in gen.STORIES] + [e[0] for e in gen.EXISTING] + assert len(ids) == len(set(ids)), "duplicate story id" + + +def test_every_story_is_assigned_to_a_real_member(gen): + """Nothing here is owned by nobody -- that is how a backlog rots.""" + members = {gen.A, gen.N, gen.S, gen.K} + for row in gen.STORIES: + assert row[5] in members, f"{row[0]} assigned to {row[5]!r}" + + +def test_every_story_states_why_it_is_in_the_milestone(gen): + """A story with no argument behind it is a wish, and gets cut.""" + for row in gen.STORIES: + assert row[3], f"{row[0]} has no acceptance criteria" + assert len(row[4].split()) >= 12, f"{row[0]} has no real rationale" diff --git a/tools/make_phase3_backlog.py b/tools/make_phase3_backlog.py new file mode 100644 index 0000000..f0f3f24 --- /dev/null +++ b/tools/make_phase3_backlog.py @@ -0,0 +1,841 @@ +"""ONE source for the Phase 3 backlog. It writes the document AND creates the +issues, so the two cannot disagree. + +That is not tidiness. This project's recurring defect -- named in CLAUDE.md, +in the final report, and in three separate post-mortems -- is one fact recorded +in two places with only one of them updated. A backlog written in a document +and then retyped into GitHub is that defect with a fresh coat of paint. So the +stories live here once, and both artefacts are generated. + +WHERE THE STORIES COME FROM + US-20..US-39 Discussion #296, unchanged. They were designed, argued and + never turned into work. Their acceptance criteria are copied + from the discussion rather than rewritten, so anyone who + reviewed the discussion has already reviewed the issue. + US-40..US-56 New. Each closes a gap a paying customer hits in the first + week, or one a buyer's security review asks about and we + currently cannot answer. Nothing is here because it sounds + impressive. + +WHAT IS DELIBERATELY NOT CREATED + Three stories already exist as issues. Creating them again would be the + duplicate-fact defect committed by the script written to avoid it: + US-28 -> #78 the client's real pfSense export + US-53 -> #220 one real compliance benchmark pack (CIS Cisco IOS) + US-57 -> #308 coverage on every surface or none + They are folded into the milestone instead, keeping their history. + +THE SPLIT, STATED RATHER THAN ARRANGED + Arsh asked for the other three to carry small slices so the critical path + stays short. That is a legitimate call for a SCRUM Master. It is made in + the open: this file says so, the milestone says so, the document says so, + and #249 -- where Arsh published his own over-authorship as a fairness + problem -- gets a comment recording that the ratio is being widened on + purpose and what it costs. +""" +import json +import pathlib +import re +import subprocess +import sys + +REPO = "ARSH871-bot/Netwise" +DOC = (pathlib.Path(__file__).resolve().parent.parent + / "docs" / "design" / "phase-3-backlog.md") +NUMBERS = DOC.with_suffix(".issues.json") +MILESTONE = "Phase 3 - market readiness" +A, N, S, K = "ARSH871-bot", "patelankeet2", "shubhamkataria2005", "SamikaPerera" +NAME = {A: "Arsh", N: "Ankeet", S: "Shubham", K: "Samika"} + +# id, title, story, [acceptance], why, assignee, epic, size, refs +STORIES = [ + # ------------------------------------------------------------ from #296 ---- + ("US-20", "Check my configs in CI, before the change merges", + "As a network engineer, I want Netwise to run in my pipeline and fail the " + "build when a change introduces a high-severity finding, so a bad rule never " + "reaches a device.", + ["`python -m netwise check ` exits non-zero when a `found` finding is at " + "or above a configurable severity, zero otherwise", + "`--format json` writes the F-1 findings to stdout unchanged", + "A `status=\"error\"` finding fails the build by default -- *we could not " + "check* must never be a silent pass in automation", + "Documented as a GitHub Actions snippet in `README.md`"], + "Turns the tool from something you remember to run into something that runs " + "itself. Of everything in this milestone it is the one that changes how the " + "product is used rather than what it can see.", + A, "Automation", "M", [], "#296"), + + ("US-21", "Analyse my whole network at once, not device by device", + "As an engineer with forty switches, I want one scan over all of them, so I " + "see problems that only exist between devices.", + ["**Already true, verified:** a snapshot of N devices is analysed, scoped " + "per device, with wall-clock recorded at 10 and 50 in `docs/scale.md`", + "Still missing: 100 devices, and MEMORY at every size -- only time was taken", + "Still missing: the finding that matters. At 50 devices the analysis " + "returns 3 findings, the same as at one, because every rule names a " + "specific device. Fifty devices in, one device examined", + "So the real deliverable is that a multi-device scan reports coverage " + "honestly: N devices present, M examined, and the difference stated", + "`tests/fixtures/multi-device-10` and `tools/make_scale_fixture.py` already " + "exist and are extended rather than replaced"], + "RESCOPED after reading `docs/scale.md`, which #218 already produced. " + "Discussion #296 asked for timings that had been taken six weeks earlier: " + "10 devices in 4.3 s, 50 in 6.9 s, roughly linear. Speed was never the risk. " + "The document's own second half says so -- the analysis does not grow with " + "the network, which is #87 wearing a different hat. What is genuinely " + "outstanding is memory, 100 devices, and making the coverage gap visible " + "rather than leaving a user to infer it from a suspiciously short list.", + A, "Scale", "M", [87, 218], "#296"), + + ("US-22", "Tell me only what changed since the last scan", + "As someone who scans weekly, I want the diff, so I am not re-reading forty " + "findings I already triaged.", + ["Findings persisted per snapshot with a content fingerprint, not an id " + "(ids are not stable identity -- this is #240's problem C and it blocks this)", + "Output partitions into **new**, **resolved**, **unchanged**", + "A check that has newly gone blind is its own category, never counted as " + "*resolved* -- that is F-4 arriving one level up", + "Depends on **US-33**; ship after it"], + "Needs the store #223 describes. Together they turn a one-off audit into " + "something worth running on a schedule.", + A, "UX", "M", [223], "#296"), + + ("US-23", "Make `access_control` read my policy", + "As a user with my own network, I want the access-control check to use my " + "rules, so it stops reporting \"could not check\" on every device I own.", + ["`access_control` reads `rules_in_use()` exactly as `policy_compliance` does", + "With no policy supplied, behaviour is byte-identical to today", + "`tools/stranger_config.py` re-run and the FOUND column recorded on the issue", + "**Half of #87.** The other half is US-26"], + "#87 is the largest measured gap in the product: one rename of a device " + "removes half the detection. This is half of closing it.", + A, "Honesty", "M", [87], "#296"), + + ("US-24", "Show me what Netwise could not read", + "As a user, I want to know which parts of my config were not understood, so " + "I know how much of the file the result actually covers.", + ["Parse coverage reported as a first-class number: lines recognised / total", + "Unrecognised constructs listed by kind, never silently dropped", + "Feeds the coverage section already in the exported report", + "Renders on every surface that renders findings, per #308's rule"], + "The coverage statement exists; what it does not yet know is how much of the " + "file was legible in the first place.", + A, "Honesty", "M", [308], "#296"), + + ("US-25", "Let me ask a follow-up", + "As a user, I want to ask \"and what would fix that?\" after an answer, so I " + "am not re-typing the whole question.", + ["A question may reference the previous answer's resolved entities only", + "The grounding guarantee is unchanged: every sub-query still runs for real", + "`question_understood` shows the **fully resolved** question, not my shorthand", + "This is #240's problem B, scoped"], + "The narrow scope of the query layer is what makes it safe rather than merely " + "limited (CLAUDE.md 7c). Widening it must not widen what the model decides.", + N, "AI", "L", [240], "#296"), + + ("US-26", "Make `routing` read my policy", + "As a user, I want routing checks against my devices, not `rtr-hq` and " + "`rtr-branch`.", + ["Same shape as US-23, same no-policy guarantee", + "A device named in the policy but absent from the snapshot produces a " + "could-not-check, never silence", + "The two together close #87; either alone only halves it"], + "The other half of #87. Landing only one of the two is worse than it sounds: " + "a user who supplies a policy would find one check reading it and another " + "silently naming our fixtures, which is a more confusing state than today's, " + "where both are honestly wrong in the same way.", + A, "Honesty", "M", [87], "#296"), + + ("US-27", "Analyse a vendor that is not Cisco, end to end", + "As an engineer with a mixed estate, I want Arista, NX-OS and Juniper to work " + "as well as IOS does.", + ["`vendor-asa` joins the other three: a deliberate fault, and an end-to-end " + "test that finds it. It is the only fixture Batfish reports as " + "`PARTIALLY_UNRECOGNIZED`, so this depends on resolving parse strictness", + "Any check that is IOS-specific says so as `status=\"error\"`, per F-4", + "A per-vendor support table in `README.md`, generated rather than typed -- " + "nothing a user reads currently says which vendors work", + "The table distinguishes *parses* from *produces findings*, because " + "`tests/test_vendor_fixtures.py` is emphatic that these are different claims"], + "RESCOPED after measuring, and the original scope was wrong in our favour " + "twice. Discussion #296 asked for \"the three existing vendor fixtures\" to " + "run through the full pipeline. There are four fixtures, and three of them " + "already do -- `pytest tests/test_vendor_fixtures.py` passes 10 tests " + "against real Batfish today. What remains is `vendor-asa`, which parses only " + "partially, and the fact that no user-facing document says any of this.", + N, "Vendors", "L", [], "#296"), + + ("US-29", "Explain related findings as one story", + "As a user looking at five findings caused by one bad rule, I want one " + "explanation, not five.", + ["Findings grouped by shared evidence source before explanation", + "The group explanation cites every finding in it; no finding loses its own card", + "Model still never chooses what to group -- grouping is deterministic"], + "Distinct from #238, which chains findings that are jointly exploitable. This " + "one is about not saying the same thing five times.", + A, "Trust", "M", [238], "#296"), + + ("US-30", "Tell me if my own policy contradicts itself", + "As someone writing a policy, I want to know that rule 3 requires what rule 7 " + "forbids, before I run anything against a network.", + ["Pure logic over the policy file; no Batfish, no config", + "Reports the contradicting pair with both rule numbers and the overlap", + "A contradiction blocks the scan rather than producing arbitrary results", + "This is #240's problem D, and it is the one that needs no network at all"], + "The only story here that can be built and tested with nothing running -- no " + "Docker, no Batfish, no Ollama.", + S, "Policy", "M", [240], "#296"), + + ("US-31", "Check a proposed change against my whole policy", + "As an engineer, I want to know whether an edit breaks any rule I have, not " + "just the one flow I thought about.", + ["`analyse_change` runs every policy rule across before and after", + "Direction reported per rule: opening, tightening, unchanged", + "A change that resolves one breach and creates another reports both", + "Extends the existing `compareFilters` + `differentialReachability` pairing"], + "Change impact and policy compliance both exist on `main` today and never " + "meet.", + S, "Policy", "M", [], "#296"), + + ("US-32", "Tell me which of my policy rules never fire", + "As a policy author, I want dead rules found in my policy the way dead ACL " + "lines are found in my config.", + ["A rule that no flow in the snapshot can match is reported as a finding", + "Reported as **hygiene**, never as a violation", + "Distinguished from a rule that matched and passed", + "Symmetric with `filterLineReachability`, and states that symmetry"], + "`filterLineReachability` already does this for the config. Nothing does it " + "for the rules the user wrote.", + S, "Policy", "S", [], "#296"), + + ("US-33", "Recognise the same problem across two scans", + "As someone tracking progress, I want to know whether this is last week's " + "problem or a new one.", + ["A content fingerprint over structured fields plus a similarity measure on " + "the free-text evidence", + "Measured against the known collision: `rtr-us5-insecure` and `rtr-us5-messy` " + "both emit `AC-001` for different problems", + "**US-22 depends on this.** This is #240's problem C"], + "A hard prerequisite for US-22 and US-46. Nothing that compares two scans is " + "trustworthy until identity is stable.", + A, "Policy", "M", [240], "#296"), + + ("US-34", "Draft a policy from a config I already have", + "As a new user with no written policy, I want a starting point derived from " + "what my firewall already does.", + ["Produces a policy file, clearly marked as a draft, never applied automatically", + "Every generated rule cites the config line it came from", + "Refuses where the config is ambiguous instead of guessing"], + "The blank page is the main reason a user supplies no policy at all -- which " + "is the gap US-23 and US-26 exist to close.", + A, "Content", "M", [87], "#296"), + + ("US-35", "Sign in, so my scans are mine", + "As one of several users on a shared install, I want my configs and results " + "private to me.", + ["Real identity on top of the session isolation from #242", + "A user can only read their own snapshots; enforced server-side and tested " + "by trying to read someone else's", + "No credential is ever logged, including on failure", + "Sessions expire"], + "Session isolation exists. There is no identity behind it, so there is " + "nothing to isolate *to*.", + K, "Multi-tenant", "L", [], "#296"), + + ("US-36", "Rank by what it would cost me, not just what kind of bug it is", + "As an owner, I want the finding on my payroll server above the same finding " + "on a lab switch.", + ["Extends `business_context.py` beyond three tiers to a stated impact model", + "The ruleset is written down before it is coded, the way `severity-rules.md` was", + "Every position is explainable: traceable to a stated rule", + "`risk` still may not downgrade or drop a `status=\"error\"` finding"], + "Extends #254, which proved the mechanism on three tiers and one level of " + "escalation. What is missing is the model behind it: today a device is " + "critical or it is not, and every finding on it moves by the same single " + "step regardless of what the finding actually is.", + A, "UX", "M", [], "#296"), + + ("US-37", "Give me something I can hand to an auditor", + "As someone facing an audit, I want a dated evidence pack, not a screenshot.", + ["Export includes: what was checked, what could not be checked, the policy " + "used, the tool version, and the snapshot fingerprint", + "The \"could not check\" section appears **first**, as it already does in the report", + "Deterministic: the same snapshot and policy produce a byte-identical pack", + "Signed or checksummed so it cannot be quietly edited after the fact"], + "The export exists since #233. What is missing is the framing that lets it " + "survive the meeting it is carried into.", + A, "UX", "M", [], "#296"), + + ("US-38", "Let me use it with a keyboard and a screen reader", + "As a user who does not use a mouse, I want the whole dashboard operable.", + ["Every control reachable and operable by keyboard, with a visible focus state", + "Landmarks and headings correct -- #284 did this for the four areas; this " + "finishes it for the cards, filters and dialogs", + "Colour is never the only carrier of severity or status", + "An automated check in CI so it cannot regress silently"], + "Begun in #284. Also a procurement requirement anywhere public money is " + "involved, so it is a commercial gate as well as a right one.", + K, "UX", "M", [], "#296"), + + ("US-39", "Show me progress during a long scan", + "As a user, I want to see what is happening while a scan runs, so I can tell " + "work from a hang.", + ["Per-stage progress: parsing, model build, each check", + "An estimate that is honest about being an estimate", + "A stalled stage is reported rather than displayed as still working"], + "The first scan of a session already costs about 22 seconds of engine warm-up " + "with no feedback at all.", + K, "UX", "S", [], "#296"), + + # -------------------------------------------------------------- new ------- + ("US-40", "Prove that nothing left my machine", + "As a security reviewer, I want a record of every outbound connection Netwise " + "attempted, so I can verify the offline claim instead of believing it.", + ["A runtime egress record naming host, port and the component that tried", + "An allowlist covering only the local Batfish and the local Ollama", + "Anything outside it is **refused** and recorded, not merely recorded", + "`python -m tools.egress_audit` prints the record for a scan", + "A test that plants an outbound call in a check and proves it is refused"], + "Constraint N-1 is the product's main commercial differentiator, and today it " + "rests entirely on code review. Every other claim in this project is measured; " + "this one is asserted. A buyer's security team will ask, and *we read the " + "source carefully* is not an answer.", + A, "Honesty", "M", [], "new"), + + ("US-41", "Install Netwise with one command", + "As an IT administrator, I want a single command that brings the whole product " + "up, so I do not need to clone a repository to try it.", + ["`docker compose up` starts app, Batfish and Ollama with pinned image tags", + "No git clone, no manual pip install, no separate model build step", + "First run works on a machine that has only Docker", + "The README's eight steps become one, with the long form kept for developers"], + "Installation today assumes a developer. It is the single largest barrier " + "between this tool and anybody who might evaluate it.", + A, "Distribution", "M", [], "new"), + + ("US-42", "Upgrade without losing my data", + "As an administrator, I want to move to a new version and keep my policies, " + "history and settings, so upgrading is not a reason to avoid upgrading.", + ["Versioned releases with semantic version numbers", + "A documented and tested upgrade path between adjacent versions", + "Stored data carries a schema version and is migrated forward", + "A downgrade is refused, never allowed to corrupt"], + "Nothing persists across a restart today, so this is free now and expensive " + "the moment US-22 or US-35 lands.", + A, "Distribution", "M", [], "new"), + + ("US-43", "Tell me how big a network you can actually handle", + "As an engineer with a real estate, I want a stated supported ceiling, so I " + "am not the person who discovers it.", + ["A generated fixture estate of 500 devices in the repository", + "Measured and published: time, memory, and where it degrades", + "A documented supported ceiling rather than an unstated one", + "Beyond it, a clear refusal -- never a partial result presented as whole"], + "`docs/scale.md` already measures 10 and 50 devices and is careful to call " + "its figure \"a floor, not an estimate\" -- the fixtures are fifty copies of " + "one router with no topology between them. This is the story that turns that " + "floor into a number we are willing to put in writing, on an estate that is " + "not synthetic. Unknown is not sellable, and a silent partial result is F-4 " + "at estate scale.", + A, "Scale", "L", [], "new"), + + ("US-44", "Run a long scan in the background", + "As a user, I want to start a scan, close the tab, and come back to the " + "result, so a large scan is not hostage to my browser.", + ["Scans become jobs with an id, a status and a result", + "A job survives an application restart", + "Progress is reported while it runs (US-39)", + "A job that dies is reported as **failed**, never as complete with no findings"], + "The request-response model caps the product at whatever fits inside one HTTP " + "timeout. The last acceptance line is the one that matters: a crashed scan " + "rendering as a clean result is the worst failure this product could have.", + A, "Scale", "L", [], "new"), + + ("US-45", "Give my team shared access, with roles", + "As a team lead, I want an organisation with members, so we share policies and " + "history instead of each keeping our own copy.", + ["Organisations own policies, scan history and business context", + "Roles: admin, member, read-only", + "An invitation flow that does not require sharing a password", + "Every destructive action records who did it in the audit log"], + "Depends on US-35. This is the difference between a utility one engineer runs " + "and a product a company buys a seat of.", + A, "Multi-tenant", "L", [], "new"), + + ("US-46", "Scan on a schedule, without me starting it", + "As an engineer, I want a nightly re-scan that tells me only what changed, so " + "drift is caught without anyone remembering to look.", + ["Scheduled scans per target, configurable", + "Results retained so US-22's diff has something to compare against", + "A notification only when something changed -- never a nightly all-clear", + "A scheduled scan that failed is reported, never silently skipped"], + "Depends on US-22 and US-44. This is what makes Netwise a monitoring product " + "rather than an audit tool you run twice a year.", + A, "Automation", "M", [], "new"), + + ("US-47", "Read my configs from where they already are", + "As an engineer, I want Netwise to pull from my config backup repository, so " + "I am not copying files by hand every time.", + ["A read-only connector for a git repository of configs", + "A watched directory as a second source", + "Credentials stored encrypted, never logged, never inside a finding", + "Read-only is **enforced**, not merely intended -- a test proves a push is refused"], + "Every organisation with more than ten devices already backs its configs up " + "somewhere. Meeting them there removes the manual step that stops this being " + "used weekly.", + A, "Automation", "M", [], "new"), + + ("US-48", "Send findings to the tools my team already uses", + "As a team lead, I want high-severity findings to reach our ticketing and " + "chat, so the finding reaches a person without anyone watching a dashboard.", + ["An outbound webhook carrying F-1 findings, **off by default**", + "Explicit configuration required, and the act of enabling it is recorded in " + "the audit log", + "The offline guarantee restated precisely in the README: *the user may choose " + "to send findings out; Netwise never does so on its own*", + "Redaction options for evidence detail"], + "The one place constraint N-1 needs a documented, consenting exception -- " + "which is exactly why it must be opt-in, audited, and described in the same " + "breath as the guarantee it qualifies. This story is as much a documentation " + "task as a code one.", + A, "Automation", "M", [], "new"), + + ("US-49", "Install it with no internet at all", + "As the operator of an air-gapped network, I want an offline bundle, so I can " + "run it in the environment that needs it most.", + ["A single archive that installs with no network access", + "The language model included, with its licence terms", + "A checksum manifest so the bundle can be verified before it is trusted", + "Documented as the supported path for isolated sites"], + "The customers who most need an offline analyser are the ones who cannot " + "download one. This turns the project's core constraint into its distribution " + "channel rather than a footnote about it.", + A, "Distribution", "M", [], "new"), + + ("US-50", "Give me an SBOM and a stated dependency policy", + "As a security reviewer, I want a bill of materials and a policy on updates, " + "so I can clear this through supply-chain review.", + ["A generated SBOM published with every release", + "A written policy for how quickly advisories are acted on", + "Dependency scanning in CI, failing on a known critical", + "Netwise runs its own CVE check against its own dependencies"], + "A standard procurement gate. Dependabot already runs here; this turns it " + "from a habit into an answer we can hand over.", + A, "Trust", "S", [], "new"), + + ("US-51", "Point Netwise at its own deployment", + "As an administrator, I want the tool to check the configuration of the box it " + "runs on, so the security tool is not the weakest thing on the network.", + ["A self-check: exposed ports, default credentials, TLS, file permissions", + "Reported through the same three-state contract as everything else", + "Shipped as a first-class check, not a script somebody remembers to run"], + "A security product that has never been pointed at itself is a fair thing for " + "a buyer to ask about, and an embarrassing thing to be asked in a demo. Note " + "that `tools/preflight.py` already checks whether this machine can RUN " + "Netwise -- Python, dependencies, Docker, Batfish, Ollama, Node. That is a " + "readiness check, not a security one, and this story must extend it rather " + "than grow a second thing that looks like it.", + A, "Trust", "S", [], "new"), + + ("US-52", "Tell me what you keep, and delete it when I say", + "As a data protection officer, I want a written and enforced retention policy, " + "so I can approve this tool without a legal argument.", + ["Documented retention for uploads, findings, history and logs", + "Configurable retention, enforced by a scheduled job rather than by hope", + "A delete that genuinely removes, including derived artefacts", + "The audit log records the deletion without recording the content"], + "Any customer handling regulated data asks this in the first meeting. " + "`web/audit_log.py` already has the redaction boundary this needs.", + A, "Multi-tenant", "M", [], "new"), + + ("US-54", "Give me the fix, not just the problem", + "As an engineer, I want a concrete config change that resolves a finding, so " + "I do not have to work out the remedy myself.", + ["A generated fix per finding, where one can be derived", + "Shown as a diff against the uploaded config -- **never applied**", + "Simulated through `analyse_change` before it is offered", + "A finding with no safe derivable fix says so rather than guessing"], + "#221 delivered remediation *text*. This is the change itself. The line that " + "must not move is CLAUDE.md 4: generate and simulate, never push.", + A, "Remediation", "L", [], "new"), + + ("US-55", "Show me the explanation layer is any good", + "As a supervisor, I want a measured evaluation of explanation quality, so the " + "AI claim rests on evidence like everything else here.", + ["A held-out set of findings with human-rated reference explanations", + "A repeatable score, run in CI, with the number published", + "A documented threshold below which the deterministic fallback is preferred"], + "The roadmap names this as Tier 2.1. The explanation layer is the one " + "component in this product with no measurement behind it, in a project whose " + "whole argument is that claims should be measured.", + N, "AI", "M", [], "new"), + + ("US-56", "Make my first five minutes work", + "As someone evaluating the tool, I want a guided first run with sample data, " + "so I see what it does before I have to supply anything.", + ["A bundled sample network with planted faults, one click to scan", + "Empty states that say what to do next rather than showing nothing", + "Every error state says what happened and what to try", + "The sample is labelled so it can never be mistaken for real data"], + "Every evaluation of this product currently begins with a blank screen and a " + "file picker. This is the cheapest story in the milestone and probably the " + "one that changes the most minds.", + A, "UX", "S", [], "new"), +] + +# Already issues. Folded into the milestone rather than created again. +EXISTING = [ + ("US-28", 78, N, "Convert my whole pfSense export, including NAT", + "Vendors", "L"), + ("US-53", 220, A, "One real compliance benchmark pack (CIS Cisco IOS)", + "Content", "L"), + ("US-57", 308, A, "Coverage on every surface that shows findings, or none", + "Honesty", "M"), +] + +EPIC_ORDER = ["Honesty", "Distribution", "Scale", "Multi-tenant", "Automation", + "Content", "Remediation", "Vendors", "AI", "Policy", "UX", "Trust"] + +EPIC_WHY = { + "Honesty": "The product's central claim is that it never says “clean” when it " + "means “unchecked”. These are the places that claim is " + "still only half-kept.", + "Distribution": "Nobody can buy something they cannot install.", + "Scale": "Speed was measured in #218 and is not the problem: fifty devices " + "analyse in 6.9 seconds. What `docs/scale.md` found instead is that " + "the analysis does not GROW with the network -- fifty devices in, " + "one device examined. These stories are about that.", + "Multi-tenant": "One person on one laptop is a tool. Several people in one " + "company is a product.", + "Automation": "A scan somebody has to remember to run is a scan that stops " + "happening in week three.", + "Content": "Our rules are ours. A buyer wants a standard's rules, and a way " + "into writing their own.", + "Remediation": "Finding the problem is half the value. The other half is the " + "line of config that fixes it.", + "Vendors": "Four vendor fixtures exist and three of them -- Arista, Juniper " + "and NX-OS -- already produce a real finding end to end. What is " + "left is the fourth, and saying so anywhere a user can see it.", + "AI": "The layer with the most attention on it and the least measurement " + "behind it.", + "Policy": "The user's own rules are a first-class artefact and deserve the " + "same scrutiny we give their configs.", + "UX": "Most of these are cheap. Together they are the difference between a " + "demo and a thing someone uses on a Tuesday.", + "Trust": "The questions a buyer's security and procurement teams ask before " + "they ask anything about features.", +} + + +def load(assignee): + return [s for s in STORIES if s[5] == assignee] + + +def counts(): + out = {} + for who in (A, N, S, K): + out[who] = len(load(who)) + len([e for e in EXISTING if e[2] == who]) + return out + + +def issue_body(sid, title, story, acc, why, who, epic, size, refs, src): + lines = [f"*{story}*", ""] + lines.append("## Acceptance criteria") + lines.append("") + lines += [f"- {a}" for a in acc] + lines += ["", "## Why this is in the milestone", "", why, ""] + lines.append("---") + lines.append("") + lines.append(f"**Epic:** {epic} | **Size:** {size} | " + f"**Assigned:** @{who}") + if src == "#296": + lines.append("") + lines.append("Story and acceptance criteria come from **Discussion " + "#296** unchanged, so anyone who reviewed the discussion " + "has already reviewed this issue.") + if refs: + lines.append("") + lines.append("Related: " + ", ".join(f"#{r}" for r in refs)) + lines.append("") + lines.append("Part of **" + MILESTONE + "**. The distribution of this " + "milestone is deliberately uneven and is explained in " + "`docs/design/phase-3-backlog.md` and on #249.") + return "\n".join(lines) + + +# --------------------------------------------------------------- document --- +def document(): + n = counts() + total = len(STORIES) + len(EXISTING) + L = [] + w = L.append + live = {} + if NUMBERS.exists(): + live = json.loads(NUMBERS.read_text(encoding="utf-8")) + w("") + w("") + w("# Phase 3 — market readiness") + w("") + w("*Everything between Netwise today and a product that could be sold.*") + w("") + w("Phase 0 was consolidation. Phase 1 was the four capabilities. Phase 2 " + "was direction only. This is the first backlog written against a " + "different question: not *what would make this a better capstone* but " + "**what would stop a stranger who paid for it from asking for a refund**.") + w("") + w("---") + w("") + w("## Where these came from") + w("") + w("| Range | Source |") + w("|---|---|") + w("| `US-20` – `US-39` | **Discussion #296**, unchanged. Twenty stories " + "designed and argued in early September and never turned into work. " + "Their acceptance criteria are copied rather than rewritten. |") + w("| `US-40` – `US-56` | **New.** Each one closes a gap a paying customer " + "hits in the first week, or one a buyer's security review asks about and " + "we currently cannot answer. |") + w("") + w("Three stories already existed as issues and are folded in rather than " + "created again — creating them twice would be this project's own " + "recurring defect, committed by the backlog written to avoid it:") + w("") + w("| Story | Existing issue |") + w("|---|---|") + for sid, num, who, t, _e, _s in EXISTING: + w(f"| `{sid}` | [#{num}]" + f"(https://github.com/{REPO}/issues/{num}) — {t} |") + w("") + w("Nothing here is a feature because it sounds impressive. Every story " + "names the thing that is currently untrue, unmeasured or unreachable.") + w("") + w("---") + w("") + w("## The distribution, stated rather than arranged") + w("") + w(f"**{total} stories. Arsh has {n[A]}. Ankeet has {n[N]}, Shubham has " + f"{n[S]}, Samika has {n[K]}.**") + w("") + w("That is deliberate, and it is a decision rather than an accident, so it " + "is written down where the people affected by it can read it.") + w("") + w("**Why.** The remaining calendar is short and most of this milestone is " + "sequential: US-22 waits on US-33, US-45 waits on US-35, US-46 waits on " + "US-22 and US-44. A dependency chain moves at the speed of whoever is " + "slowest to pick up their link. Concentrating the chain on one person " + "removes the hand-off latency, which is the largest single cost in a " + "four-person team working part-time around other papers.") + w("") + w("**What each of the other three gets** is a short, self-contained slice " + "inside the vertical they already own, with no story in it that blocks " + "anybody else. They can finish, stop, and not be the reason something " + "else is waiting.") + w("") + w("**What it costs, which is the part worth saying out loud.** #249 " + "measured that Arsh authored 61% of pull requests and consumed 62% of " + "the team's review capacity, and — more importantly — that **review, not " + "authorship, is where this project's defects have actually been caught**. " + "The fixture corruption in #304, the student IDs in #311, the stale " + "amendment table, the false claim in the portfolio: every one was found " + "by somebody reading somebody else's work.") + w("") + w("So this split does not reduce the team's work. It moves it from " + "authoring to reviewing. **Every story here still needs a current " + "approving review from someone who did not write it (M-1), and new work " + "still voids an approval (M-2 rule 1).** If the other three treat a " + "light authoring load as a light total load, this milestone will ship " + "faster and be worse, and the failure will not be visible until " + "something built on an unreviewed foundation has to be unpicked.") + w("") + w("The honest summary: **three people are being asked to write less and " + "read more.** That is the trade, and it is only a good trade if the " + "reading actually happens.") + w("") + w("---") + w("") + w("## The backlog") + w("") + for epic in EPIC_ORDER: + rows = [r for r in STORIES if r[6] == epic] + if not rows and not [e for e in EXISTING if e[4] == epic]: + continue + w(f"### {epic}") + w("") + w(f"*{EPIC_WHY[epic]}*") + w("") + w("| | Story | Issue | Size | Owner |") + w("|---|---|---|---|---|") + merged = [(r[0], r[1], r[7], r[5], "") for r in rows] + merged += [(e[0], e[3], e[5], e[2], " *(already open)*") + for e in EXISTING if e[4] == epic] + for sid, title, size, who, note in sorted(merged): + num = live.get(sid) + link = (f"[#{num}](https://github.com/{REPO}/issues/{num})" + if num else "_not created_") + w(f"| `{sid}` | {title}{note} | {link} | {size} | {NAME[who]} |") + w("") + w("---") + w("") + w("## Order of work") + w("") + w("Not a schedule. A dependency order, so nothing is started before the " + "thing it needs exists.") + w("") + w("```") + w("1 install and honesty US-41 US-40 US-56 US-23 US-26 US-24") + w(" US-57") + w(" a stranger can run it, and it tells the truth about what it read") + w("") + w("2 identity and persistence US-35 US-42 US-33 US-22") + w(" results stop being disposable, and a finding keeps its name") + w("") + w("3 scale and automation US-21 US-43 US-44 US-20 US-46 US-47") + w(" it survives a real estate and runs without being asked") + w("") + w("4 breadth and content US-27 US-28 US-53 US-34 US-54 US-31") + w(" more vendors, real benchmarks, and the fix as well as the fault") + w("") + w("5 the buyer's questions US-50 US-51 US-52 US-45 US-37 US-48") + w(" US-49") + w(" procurement, security review, and the team-sized product") + w("") + w("throughout US-25 US-29 US-30 US-32 US-36") + w(" US-38 US-39 US-55") + w(" independent of the chain; can start any time") + w("```") + w("") + w("---") + w("") + w("## What would make this milestone a failure") + w("") + w("Worth writing down now, while it is cheap to say.") + w("") + w("1. **A story marked done that widened a claim rather than a " + "capability.** US-24, US-40 and US-43 are all one careless commit away " + "from asserting more coverage than they deliver. That is the exact " + "failure this product exists to catch in other people's networks.") + w("2. **Reviews thinning as authoring concentrates.** See the section " + "above. This is the most likely way it goes wrong.") + w("3. **This document going stale.** It is generated from a single source " + "along with the issues themselves, precisely so it cannot disagree with " + "them. If it is ever hand-edited, that guarantee is gone and it becomes " + "one more copy of a fact — which is the defect family this project " + "has been recording since the first sprint.") + w("") + text = "\n".join(L) + "\n" + + # The ordering block is written by hand, which is exactly how US-49 and + # US-57 fell out of it the first time this document was generated -- a + # list maintained beside the thing it describes, going out of step with + # it. So the block is checked against the stories rather than trusted. + block = text.split("## Order of work", 1)[1].split("```")[1] + listed = set(re.findall(r"US-\d+", block)) + every = {r[0] for r in STORIES} | {e[0] for e in EXISTING} + missing = sorted(every - listed) + stray = sorted(listed - every) + if missing or stray: + raise SystemExit( + f"order-of-work block disagrees with the backlog:\n" + f" missing: {missing or 'none'}\n" + f" unknown: {stray or 'none'}") + return text + + +def gh(args): + return subprocess.run(["gh", *args], capture_output=True, text=True, + check=True).stdout.strip() + + +def create(): + """Create every story that does not already have a number recorded. + + Re-runnable. A story already in the numbers file is skipped rather than + created again, so an interrupted run resumes instead of duplicating -- + which is the exact failure this script exists to make impossible. + """ + live = {} + if NUMBERS.exists(): + live = json.loads(NUMBERS.read_text(encoding="utf-8")) + tmp = pathlib.Path(__file__).resolve().parent / ".issue-body.md" + for row in STORIES: + sid = row[0] + if sid in live: + print(f" {sid:<7} already #{live[sid]}, skipped") + continue + tmp.write_text(issue_body(*row), encoding="utf-8") + try: + url = gh(["issue", "create", "--repo", REPO, + "--title", f"{sid} \u2014 {row[1]}", + "--body-file", str(tmp), + "--assignee", row[5], + "--milestone", MILESTONE, + "--label", "phase: 3", + "--label", f"size: {row[7]}"]) + finally: + tmp.unlink(missing_ok=True) + live[sid] = int(url.rstrip("/").rsplit("/", 1)[-1]) + NUMBERS.write_text(json.dumps(live, indent=1, sort_keys=True) + "\n", + encoding="utf-8") + print(f" {sid:<7} #{live[sid]:<5} {NAME[row[5]]:<9} {row[1][:50]}") + for sid, num, _who, _t, _e, _s in EXISTING: + live.setdefault(sid, num) + NUMBERS.write_text(json.dumps(live, indent=1, sort_keys=True) + "\n", + encoding="utf-8") + print(f"\n {len(live)} stories carry an issue number") + + +def update(): + """Push the current body of every created story back onto its issue. + + Without this, editing a story here fixes the document and leaves the + issue saying the old thing -- one fact in two places with one of them + updated, which is the defect this whole script is arranged against. + Creating the issues was never the end of the coupling; keeping them is. + """ + live = json.loads(NUMBERS.read_text(encoding="utf-8")) + tmp = pathlib.Path(__file__).resolve().parent / ".issue-body.md" + changed = 0 + for row in STORIES: + num = live.get(row[0]) + if num is None: + continue + current = gh(["issue", "view", str(num), "--repo", REPO, + "--json", "body", "--jq", ".body"]) + wanted = issue_body(*row) + if current.replace("\r\n", "\n").strip() == wanted.strip(): + continue + tmp.write_text(wanted, encoding="utf-8") + try: + gh(["issue", "edit", str(num), "--repo", REPO, + "--title", f"{row[0]} \u2014 {row[1]}", + "--body-file", str(tmp)]) + finally: + tmp.unlink(missing_ok=True) + changed += 1 + print(f" {row[0]:<7} #{num:<5} body updated") + print(f"\n {changed} issue(s) changed, " + f"{len(STORIES) - changed} already current") + + +if __name__ == "__main__": + if sys.argv[1:2] == ["create"]: + create() + elif sys.argv[1:2] == ["update"]: + update() + elif sys.argv[1:2] == ["doc"]: + out = pathlib.Path(sys.argv[2]) if sys.argv[2:3] else DOC + out.write_text(document(), encoding="utf-8") + n = counts() + print(f"wrote {out} ({len(document().splitlines())} lines)") + print(f"stories: {len(STORIES)} new + {len(EXISTING)} existing " + f"= {len(STORIES) + len(EXISTING)}") + for who in (A, N, S, K): + print(f" {NAME[who]:<9} {n[who]}") + elif sys.argv[1:2] == ["json"]: + print(json.dumps([{ + "id": s[0], "title": s[1], "body": issue_body(*s), + "assignee": s[5], "epic": s[6], "size": s[7], + } for s in STORIES], indent=1))