@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/adl +2 -2
- package/package.json +1 -1
- package/packages/agent-hooks/deploy-profile.json +1 -1
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +8 -4
- package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
- package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
- package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
- package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
- package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
- package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
- package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
- package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
- package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
- package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
- package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
- package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
- package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
- package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
- package/packages/agent-skills/dist/deploy.d.ts +7 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +52 -5
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +48 -4
- package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
- package/packages/azdo/src/activity-log.ts +118 -0
- package/packages/azdo/src/azdo-attachments.ts +108 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +194 -11
- package/packages/azdo/src/azdo-wit.ts +329 -18
- package/packages/azdo/src/cli.ts +42 -2
- package/packages/azdo/src/lib/client.ts +7 -1
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +53 -5
- package/packages/azdo/src/tool-dispatch.ts +53 -4
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/repo-controls/README.md +12 -4
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
- package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/intent.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/design.md +35 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/file-map-reference.md +19 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/intent.md +25 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +25 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/requirements.md +44 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/requirements.md +44 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
|
@@ -3,12 +3,13 @@ work item it is named for.
|
|
|
3
3
|
|
|
4
4
|
plans/
|
|
5
5
|
├── AGENTS.md
|
|
6
|
-
├──
|
|
7
|
-
│ ├── feature.md
|
|
8
|
-
│ └── backlog-item.md
|
|
6
|
+
├── LEDGER.md
|
|
9
7
|
└── features/
|
|
10
8
|
└── az<id>-<slug>/
|
|
11
|
-
├──
|
|
9
|
+
├── intent.md
|
|
10
|
+
├── requirements.md
|
|
11
|
+
├── design.md
|
|
12
|
+
├── file-map-reference.md
|
|
12
13
|
└── items/
|
|
13
14
|
└── az<id>-<slug>.md
|
|
14
15
|
|
|
@@ -25,6 +26,16 @@ and never renamed. A filename whose id disagrees with its own `work_item:` is
|
|
|
25
26
|
the one way this rots, and it rots silently: every tool keeps working, and the
|
|
26
27
|
file simply describes a different work item than its name claims.
|
|
27
28
|
|
|
29
|
+
R1–R7 (ADL process handover, 2026-10-12): `intent.md` is the one required plan
|
|
30
|
+
file — a Feature with only `intent.md` passes until its planning run writes
|
|
31
|
+
`requirements.md`, `design.md` and `file-map-reference.md` (R2, R3, R6). Plan
|
|
32
|
+
files mirror the Feature's five board states; item files keep the twelve item
|
|
33
|
+
states (R1). `GAPS-AND-FINDINGS.md` is retired from the plans root (R7). Every
|
|
34
|
+
plan file and item file carries its required-section list, `requirements.md`
|
|
35
|
+
acceptance criteria are EARS sentences, and every item file names its Feature's
|
|
36
|
+
board URL in `feature:` (R4). The file contracts ship in the workitems-process
|
|
37
|
+
skill's references, so the fix hints point there (R5).
|
|
38
|
+
|
|
28
39
|
Replaces the earlier `.plans/a_new|b_committed|c_done` lifecycle tree. That
|
|
29
40
|
design mirrored a work item's state in a directory name, which meant two
|
|
30
41
|
authorities for one fact. State now lives only on the board and files do not
|
|
@@ -58,7 +69,19 @@ class PlansLayout(Check):
|
|
|
58
69
|
FEATURES: ClassVar[str] = "features"
|
|
59
70
|
TEMPLATES: ClassVar[str] = "templates"
|
|
60
71
|
ITEMS: ClassVar[str] = "items"
|
|
61
|
-
|
|
72
|
+
|
|
73
|
+
# The four plan files. `intent.md` is required — it lands with the Feature
|
|
74
|
+
# itself — and the other three arrive with the planning run (R2).
|
|
75
|
+
INTENT: ClassVar[str] = "intent.md"
|
|
76
|
+
REQUIREMENTS: ClassVar[str] = "requirements.md"
|
|
77
|
+
DESIGN: ClassVar[str] = "design.md"
|
|
78
|
+
FILE_MAP: ClassVar[str] = "file-map-reference.md"
|
|
79
|
+
PLAN_FILES: ClassVar[tuple[str, ...]] = (
|
|
80
|
+
INTENT,
|
|
81
|
+
REQUIREMENTS,
|
|
82
|
+
DESIGN,
|
|
83
|
+
FILE_MAP,
|
|
84
|
+
)
|
|
62
85
|
|
|
63
86
|
# Supporting documents, named by ROLE and not by topic. The topic is already
|
|
64
87
|
# the directory (`az355-channels-humans-and-agents`), so a file called
|
|
@@ -67,15 +90,9 @@ class PlansLayout(Check):
|
|
|
67
90
|
# you can tell what a file is from its name, in every feature, without opening
|
|
68
91
|
# it. Adding a name here later is cheap; leaving the set open is how a tree
|
|
69
92
|
# ends up with HANDOVER.md, handover-v2.md and channels-notes.md side by side.
|
|
70
|
-
SUPPORTING: ClassVar[tuple[str, ...]] = ("handoff.md", "
|
|
93
|
+
SUPPORTING: ClassVar[tuple[str, ...]] = ("handoff.md", "review.md")
|
|
71
94
|
|
|
72
95
|
ROOT_DIRS: ClassVar[tuple[str, ...]] = ("features", "templates")
|
|
73
|
-
# `GAPS-AND-FINDINGS.md` sits here because its own rows say where they go:
|
|
74
|
-
# each becomes an item file "parented under the same Feature as the plan it
|
|
75
|
-
# was found from". It is a register OF plans work, so it belongs beside the
|
|
76
|
-
# plans, not at the repository root. `root_shape.REGISTERS` still permits a
|
|
77
|
-
# register at root for repositories that keep one there -- that is an
|
|
78
|
-
# allowlist, not a requirement, so nothing breaks by leaving.
|
|
79
96
|
# `LEDGER.md`, not `LEDGER.yaml`. Owner ruling 2026-09-17: the plan-event
|
|
80
97
|
# ledger is Markdown, because the estate already has ast-grep markdown and
|
|
81
98
|
# yq frontmatter tooling to scan it, and a YAML register beside a tree of
|
|
@@ -86,8 +103,11 @@ class PlansLayout(Check):
|
|
|
86
103
|
# Markdown form as "legacy", and this list allowed only the YAML. A
|
|
87
104
|
# repository could not satisfy all three at once -- a managed repository's conversion
|
|
88
105
|
# failed here the moment it succeeded elsewhere.
|
|
106
|
+
#
|
|
107
|
+
# R7 (2026-10-12): `GAPS-AND-FINDINGS.md` is retired with the register it
|
|
108
|
+
# held; findings enter only through report-bug.
|
|
89
109
|
ROOT_ALLOWED_FILES: ClassVar[tuple[str, ...]] = (
|
|
90
|
-
"AGENTS.md", "CLAUDE.md", "LEDGER.md",
|
|
110
|
+
"AGENTS.md", "CLAUDE.md", "LEDGER.md",
|
|
91
111
|
)
|
|
92
112
|
IGNORED: ClassVar[str] = ".DS_Store"
|
|
93
113
|
KEEP: ClassVar[str] = ".gitkeep"
|
|
@@ -97,23 +117,71 @@ class PlansLayout(Check):
|
|
|
97
117
|
ITEM_FILE: ClassVar[re.Pattern[str]] = re.compile(r"^az([0-9]+)-[a-z0-9]+(-[a-z0-9]+)*\.md$")
|
|
98
118
|
|
|
99
119
|
WORK_ITEM_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.WORK_ITEM_LINE
|
|
120
|
+
FEATURE_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.FEATURE_LINE
|
|
100
121
|
EPIC_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^epic:\s*(.+)$")
|
|
101
122
|
TYPE_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^type:\s*(.+)$")
|
|
102
123
|
TITLE_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^title:\s*(.+)$")
|
|
103
124
|
STATUS_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.STATUS_LINE
|
|
104
125
|
MODIFIED_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^last_update:\s*(.+)$")
|
|
105
|
-
SOURCES_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^sources:\s*$")
|
|
106
126
|
|
|
107
|
-
|
|
127
|
+
# The frontmatter `type:` each plan file declares.
|
|
128
|
+
INTENT_TYPE: ClassVar[str] = "intent"
|
|
108
129
|
FEATURE_TYPE: ClassVar[str] = "feature"
|
|
130
|
+
DESIGN_TYPE: ClassVar[str] = "design"
|
|
131
|
+
FILE_MAP_TYPE: ClassVar[str] = "file-map"
|
|
132
|
+
|
|
133
|
+
ITEM_TYPES: ClassVar[tuple[str, ...]] = ("pbi", "bug")
|
|
109
134
|
|
|
110
135
|
# The board's own states. `status` here mirrors the work item, not a
|
|
111
|
-
# document lifecycle, so `draft` and `stable` are wrong in this tree.
|
|
136
|
+
# document lifecycle, so `draft` and `stable` are wrong in this tree. Plan
|
|
137
|
+
# files mirror the FEATURE's state, item files the ITEM's (R1).
|
|
112
138
|
#
|
|
113
139
|
# This gate checks the value is a REAL state; it cannot check the value is
|
|
114
140
|
# the CURRENT one, because that needs the board. A file whose status has
|
|
115
141
|
# gone stale is the known cost of keeping the field at all.
|
|
116
|
-
|
|
142
|
+
FEATURE_STATUSES: ClassVar[tuple[str, ...]] = PlanMetadata.FEATURE_STATUSES
|
|
143
|
+
ITEM_STATUSES: ClassVar[tuple[str, ...]] = PlanMetadata.ITEM_STATUSES
|
|
144
|
+
|
|
145
|
+
# Required sections, per process-design.md § Plan files / § Item files
|
|
146
|
+
# (R4). One finding per missing section, named so the author can fix the
|
|
147
|
+
# file without opening the check.
|
|
148
|
+
INTENT_SECTIONS: ClassVar[tuple[str, ...]] = (
|
|
149
|
+
"## Why", "## Outcome", "## Constraints", "## Non-goals",
|
|
150
|
+
)
|
|
151
|
+
REQUIREMENTS_SECTIONS: ClassVar[tuple[str, ...]] = (
|
|
152
|
+
"## Problem", "## Acceptance criteria", "## Scope", "## Predecessors",
|
|
153
|
+
"## Items", "## Open questions for G1", "## Revision log",
|
|
154
|
+
)
|
|
155
|
+
DESIGN_SECTIONS: ClassVar[tuple[str, ...]] = (
|
|
156
|
+
"## Solution", "## Decisions", "## Verification", "## Docs impact",
|
|
157
|
+
"## Size estimate", "## Risks",
|
|
158
|
+
)
|
|
159
|
+
FILE_MAP_SECTIONS: ClassVar[tuple[str, ...]] = ("## Baseline", "## Map")
|
|
160
|
+
# A type: bug item file is a Fixes Bug item file -- Bugs are never planned,
|
|
161
|
+
# so the bugfix shape is the only bug shape there is.
|
|
162
|
+
PBI_SECTIONS: ClassVar[tuple[str, ...]] = ("## What it delivers", "## Verification")
|
|
163
|
+
BUG_SECTIONS: ClassVar[tuple[str, ...]] = (
|
|
164
|
+
"## Current behaviour", "## Expected behaviour",
|
|
165
|
+
"## Behaviour that must not change", "## Responsible Feature",
|
|
166
|
+
"## Predecessors", "## Verification",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# EARS (R4): every numbered acceptance criterion is one sentence of the
|
|
170
|
+
# form "THE SYSTEM SHALL …", optionally opened by WHEN, WHILE, IF … THEN
|
|
171
|
+
# or WHERE. One finding per offending criterion.
|
|
172
|
+
# A criterion is an AC-n reference followed by a sentence delimiter; prose
|
|
173
|
+
# that merely mentions AC-1 ("AC-1 is the load-bearing criterion") is not
|
|
174
|
+
# one and is not EARS-checked.
|
|
175
|
+
AC_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^\s*(?:[-*]\s+)?AC-([0-9]+)[.:)]\s+(.+?)\s*$")
|
|
176
|
+
EARS: ClassVar[re.Pattern[str]] = re.compile(
|
|
177
|
+
r"^(?:(?:WHEN|WHILE|WHERE)\b.+?|IF\b.+?THEN\b.+?)?THE\s+SYSTEM\s+SHALL\b",
|
|
178
|
+
re.IGNORECASE,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# The board's own states, as `sources:` list entries. `requirements.md`
|
|
182
|
+
# derives from `intent.md` (listed first); `design.md` derives from
|
|
183
|
+
# `requirements.md`.
|
|
184
|
+
SOURCE_ITEM: ClassVar[re.Pattern[str]] = re.compile(r"^\s*-\s+(.+?)\s*$")
|
|
117
185
|
|
|
118
186
|
# A work item reference is the URL you can click, not a bare id: the id is
|
|
119
187
|
# already in the filename, so repeating it as a number adds nothing, while
|
|
@@ -220,7 +288,7 @@ class PlansLayout(Check):
|
|
|
220
288
|
# Derived from the constant, never restated. The hardcoded
|
|
221
289
|
# version had drifted twice over: it named `LEDGER.yaml`,
|
|
222
290
|
# which is no longer allowed, and omitted
|
|
223
|
-
# `GAPS-AND-FINDINGS.md`, which
|
|
291
|
+
# `GAPS-AND-FINDINGS.md`, which was -- so it was wrong about
|
|
224
292
|
# both ends of the same four-item list it was describing.
|
|
225
293
|
f"direct plans files must be one of: {', '.join(self.ROOT_ALLOWED_FILES)}",
|
|
226
294
|
f"move {entry} into its Feature directory under "
|
|
@@ -289,28 +357,45 @@ class PlansLayout(Check):
|
|
|
289
357
|
def _feature_contents(
|
|
290
358
|
self, ctx: Context, feature_dir: str, feature_id: int, given: set[str]
|
|
291
359
|
) -> Iterator[Finding]:
|
|
292
|
-
|
|
293
|
-
if not ctx.path(
|
|
360
|
+
intent = f"{feature_dir}/{self.INTENT}"
|
|
361
|
+
if not ctx.path(intent).is_file():
|
|
294
362
|
yield self.report(
|
|
295
|
-
|
|
296
|
-
"Feature directory must hold
|
|
297
|
-
|
|
363
|
+
intent,
|
|
364
|
+
"Feature directory must hold an intent.md",
|
|
365
|
+
# R5: the file contracts ship in the workitems-process
|
|
366
|
+
# skill's references, not a per-repo plans/templates/.
|
|
367
|
+
f"add {intent} per the workitems-process skill's "
|
|
368
|
+
"references/ado-and-git.md § 4",
|
|
298
369
|
)
|
|
299
370
|
else:
|
|
300
|
-
yield from self.
|
|
371
|
+
yield from self._check_intent(ctx, intent, feature_id)
|
|
372
|
+
|
|
373
|
+
# requirements.md, design.md and file-map-reference.md arrive with
|
|
374
|
+
# the planning run (R2); each is graded only once it exists.
|
|
375
|
+
for name, check in (
|
|
376
|
+
(self.REQUIREMENTS, self._check_requirements),
|
|
377
|
+
(self.DESIGN, self._check_design),
|
|
378
|
+
(self.FILE_MAP, self._check_file_map),
|
|
379
|
+
):
|
|
380
|
+
path = f"{feature_dir}/{name}"
|
|
381
|
+
if ctx.path(path).is_file():
|
|
382
|
+
yield from check(ctx, path, feature_id)
|
|
301
383
|
|
|
302
384
|
for entry in sorted(self._under(feature_dir, given)):
|
|
303
385
|
name = entry.rsplit("/", 1)[-1]
|
|
304
|
-
if name in (self.IGNORED, self.
|
|
386
|
+
if name in (self.IGNORED, *self.PLAN_FILES, self.ITEMS):
|
|
305
387
|
continue
|
|
306
388
|
if name in self.SUPPORTING:
|
|
307
389
|
# A supporting document carries the same frontmatter as everything
|
|
308
390
|
# else in this tree. Without it a handoff is an undated wall of prose
|
|
309
391
|
# that names no work item -- readable today, unattributable in a month,
|
|
310
392
|
# which is the state CHANNELS-HANDOVER.md was in at the repository root.
|
|
311
|
-
yield from self._common(
|
|
393
|
+
yield from self._common(
|
|
394
|
+
ctx, entry, self.text(ctx, entry) or "", feature_id,
|
|
395
|
+
self.ITEM_STATUSES,
|
|
396
|
+
)
|
|
312
397
|
continue
|
|
313
|
-
allowed = ", ".join((self.
|
|
398
|
+
allowed = ", ".join((*self.PLAN_FILES, *self.SUPPORTING, f"{self.ITEMS}/"))
|
|
314
399
|
yield self.report(
|
|
315
400
|
entry,
|
|
316
401
|
f"a Feature directory holds {allowed} only",
|
|
@@ -339,11 +424,34 @@ class PlansLayout(Check):
|
|
|
339
424
|
def _field(body: str, pattern: re.Pattern[str]) -> str | None:
|
|
340
425
|
return PlanMetadata.field(body, pattern)
|
|
341
426
|
|
|
342
|
-
def
|
|
343
|
-
"""The
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
427
|
+
def _sources(self, body: str) -> list[str] | None:
|
|
428
|
+
"""The frontmatter `sources:` list, or None when the key is absent."""
|
|
429
|
+
lines = body.splitlines()
|
|
430
|
+
if not lines or lines[0].strip() != "---":
|
|
431
|
+
return None
|
|
432
|
+
items: list[str] = []
|
|
433
|
+
in_sources = False
|
|
434
|
+
for line in lines[1:]:
|
|
435
|
+
if line.strip() == "---":
|
|
436
|
+
break
|
|
437
|
+
if in_sources:
|
|
438
|
+
found = self.SOURCE_ITEM.match(line)
|
|
439
|
+
if found:
|
|
440
|
+
items.append(found.group(1).strip().strip("\"'"))
|
|
441
|
+
elif line.strip():
|
|
442
|
+
break
|
|
443
|
+
elif re.match(r"^sources:\s*$", line):
|
|
444
|
+
in_sources = True
|
|
445
|
+
return items if in_sources else None
|
|
446
|
+
|
|
447
|
+
def _common(
|
|
448
|
+
self,
|
|
449
|
+
ctx: Context,
|
|
450
|
+
path: str,
|
|
451
|
+
body: str,
|
|
452
|
+
expected_id: int,
|
|
453
|
+
statuses: tuple[str, ...],
|
|
454
|
+
) -> Iterator[Finding]:
|
|
347
455
|
"""The frontmatter every document in this tree carries."""
|
|
348
456
|
if self._field(body, self.TITLE_LINE) is None:
|
|
349
457
|
yield self.report(path, "missing title", f"add `title:` to {path}")
|
|
@@ -353,13 +461,13 @@ class PlansLayout(Check):
|
|
|
353
461
|
yield self.report(
|
|
354
462
|
path,
|
|
355
463
|
"missing status",
|
|
356
|
-
f"add `status:` to {path}, one of: {', '.join(
|
|
464
|
+
f"add `status:` to {path}, one of: {', '.join(statuses)}",
|
|
357
465
|
)
|
|
358
|
-
elif status not in
|
|
466
|
+
elif status not in statuses:
|
|
359
467
|
yield self.report(
|
|
360
468
|
path,
|
|
361
469
|
f"status {status!r} is not a work item state",
|
|
362
|
-
f"set status in {path} to one of: {', '.join(
|
|
470
|
+
f"set status in {path} to one of: {', '.join(statuses)}; "
|
|
363
471
|
"it mirrors the board, not a document lifecycle",
|
|
364
472
|
)
|
|
365
473
|
|
|
@@ -375,7 +483,7 @@ class PlansLayout(Check):
|
|
|
375
483
|
f"set last_update in {path} to YYYY-MM-DDTHH:MM:SSZ",
|
|
376
484
|
)
|
|
377
485
|
|
|
378
|
-
found_id, why =
|
|
486
|
+
found_id, why = PlanMetadata.work_item_id(body)
|
|
379
487
|
if why == "missing":
|
|
380
488
|
yield self.report(
|
|
381
489
|
path,
|
|
@@ -398,46 +506,136 @@ class PlansLayout(Check):
|
|
|
398
506
|
f"az{found_id}",
|
|
399
507
|
)
|
|
400
508
|
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
509
|
+
def _sections(
|
|
510
|
+
self, path: str, body: str, required: tuple[str, ...]
|
|
511
|
+
) -> Iterator[Finding]:
|
|
512
|
+
"""One finding per required section the document does not carry."""
|
|
513
|
+
headings = {line.strip() for line in body.splitlines()}
|
|
514
|
+
for section in required:
|
|
515
|
+
if section not in headings:
|
|
516
|
+
yield self.report(
|
|
517
|
+
path,
|
|
518
|
+
f"missing section {section}",
|
|
519
|
+
f"add a '{section}' section to {path}",
|
|
520
|
+
)
|
|
404
521
|
|
|
522
|
+
def _check_type(self, path: str, body: str, want: str) -> Iterator[Finding]:
|
|
405
523
|
declared = self._field(body, self.TYPE_LINE)
|
|
406
|
-
if declared !=
|
|
524
|
+
if declared != want:
|
|
407
525
|
yield self.report(
|
|
408
|
-
|
|
409
|
-
f"type must be {
|
|
410
|
-
f"set `type: {
|
|
526
|
+
path,
|
|
527
|
+
f"type must be {want!r} (got {declared!r})",
|
|
528
|
+
f"set `type: {want}` in {path}",
|
|
411
529
|
)
|
|
412
530
|
|
|
531
|
+
# -------------------------------------------------------------- plan files
|
|
532
|
+
|
|
533
|
+
def _check_intent(
|
|
534
|
+
self, ctx: Context, intent: str, feature_id: int
|
|
535
|
+
) -> Iterator[Finding]:
|
|
536
|
+
body = self.text(ctx, intent) or ""
|
|
537
|
+
yield from self._common(ctx, intent, body, feature_id, self.FEATURE_STATUSES)
|
|
538
|
+
yield from self._check_type(intent, body, self.INTENT_TYPE)
|
|
539
|
+
yield from self._sections(intent, body, self.INTENT_SECTIONS)
|
|
540
|
+
|
|
541
|
+
def _check_requirements(
|
|
542
|
+
self, ctx: Context, requirements: str, feature_id: int
|
|
543
|
+
) -> Iterator[Finding]:
|
|
544
|
+
body = self.text(ctx, requirements) or ""
|
|
545
|
+
yield from self._common(
|
|
546
|
+
ctx, requirements, body, feature_id, self.FEATURE_STATUSES
|
|
547
|
+
)
|
|
548
|
+
yield from self._check_type(requirements, body, self.FEATURE_TYPE)
|
|
549
|
+
|
|
413
550
|
epic = self._field(body, self.EPIC_LINE)
|
|
414
551
|
if epic is None:
|
|
415
552
|
yield self.report(
|
|
416
|
-
|
|
417
|
-
"
|
|
418
|
-
f"add `epic:` to {
|
|
553
|
+
requirements,
|
|
554
|
+
"requirements.md must name the Epic its Feature hangs under",
|
|
555
|
+
f"add `epic:` to {requirements} as the board URL; it is authored, never "
|
|
419
556
|
"tool-created, so a wrong id fails loudly at parenting time",
|
|
420
557
|
)
|
|
421
558
|
elif not self.WORK_ITEM_URL.match(epic):
|
|
422
559
|
yield self.report(
|
|
423
|
-
|
|
560
|
+
requirements,
|
|
424
561
|
"epic must be a board URL, not a bare id",
|
|
425
|
-
f"set epic in {
|
|
562
|
+
f"set epic in {requirements} to https://dev.azure.com/.../_workitems/edit/<id>",
|
|
426
563
|
)
|
|
427
564
|
|
|
428
|
-
|
|
565
|
+
sources = self._sources(body)
|
|
566
|
+
if sources is None:
|
|
429
567
|
yield self.report(
|
|
430
|
-
|
|
431
|
-
"
|
|
432
|
-
f"add a `sources:` list to {
|
|
433
|
-
"
|
|
568
|
+
requirements,
|
|
569
|
+
"requirements.md must link the documents it derives from",
|
|
570
|
+
f"add a `sources:` list to {requirements} pointing at intent.md; "
|
|
571
|
+
"a requirements file with none cannot have its reasoning retraced",
|
|
434
572
|
)
|
|
573
|
+
elif not sources or "intent.md" not in sources[0]:
|
|
574
|
+
yield self.report(
|
|
575
|
+
requirements,
|
|
576
|
+
"requirements.md must list intent.md first in sources",
|
|
577
|
+
f"set the first `sources:` entry of {requirements} to intent.md",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
yield from self._sections(requirements, body, self.REQUIREMENTS_SECTIONS)
|
|
581
|
+
yield from self._check_ears(requirements, body)
|
|
582
|
+
|
|
583
|
+
def _check_design(
|
|
584
|
+
self, ctx: Context, design: str, feature_id: int
|
|
585
|
+
) -> Iterator[Finding]:
|
|
586
|
+
body = self.text(ctx, design) or ""
|
|
587
|
+
yield from self._common(ctx, design, body, feature_id, self.FEATURE_STATUSES)
|
|
588
|
+
yield from self._check_type(design, body, self.DESIGN_TYPE)
|
|
589
|
+
|
|
590
|
+
sources = self._sources(body)
|
|
591
|
+
if sources is None:
|
|
592
|
+
yield self.report(
|
|
593
|
+
design,
|
|
594
|
+
"design.md must link the documents it derives from",
|
|
595
|
+
f"add a `sources:` list to {design} pointing at requirements.md",
|
|
596
|
+
)
|
|
597
|
+
elif not any("requirements.md" in source for source in sources):
|
|
598
|
+
yield self.report(
|
|
599
|
+
design,
|
|
600
|
+
"design.md must list requirements.md in sources",
|
|
601
|
+
f"add requirements.md to the `sources:` list of {design}",
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
yield from self._sections(design, body, self.DESIGN_SECTIONS)
|
|
605
|
+
|
|
606
|
+
def _check_file_map(
|
|
607
|
+
self, ctx: Context, file_map: str, feature_id: int
|
|
608
|
+
) -> Iterator[Finding]:
|
|
609
|
+
body = self.text(ctx, file_map) or ""
|
|
610
|
+
yield from self._common(ctx, file_map, body, feature_id, self.FEATURE_STATUSES)
|
|
611
|
+
yield from self._check_type(file_map, body, self.FILE_MAP_TYPE)
|
|
612
|
+
yield from self._sections(file_map, body, self.FILE_MAP_SECTIONS)
|
|
613
|
+
|
|
614
|
+
# -------------------------------------------------------------------- EARS
|
|
615
|
+
|
|
616
|
+
def _check_ears(self, requirements: str, body: str) -> Iterator[Finding]:
|
|
617
|
+
for number, line in enumerate(body.splitlines(), start=1):
|
|
618
|
+
found = self.AC_LINE.match(line)
|
|
619
|
+
if not found:
|
|
620
|
+
continue
|
|
621
|
+
if self.EARS.match(found.group(2)):
|
|
622
|
+
continue
|
|
623
|
+
yield self.report(
|
|
624
|
+
requirements,
|
|
625
|
+
f"AC-{found.group(1)} is not an EARS sentence",
|
|
626
|
+
f"rewrite AC-{found.group(1)} in {requirements} as "
|
|
627
|
+
"'THE SYSTEM SHALL …', optionally opened by WHEN / WHILE / "
|
|
628
|
+
"IF … THEN / WHERE",
|
|
629
|
+
line=number,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# -------------------------------------------------------------- item files
|
|
435
633
|
|
|
436
634
|
def _check_item(
|
|
437
635
|
self, ctx: Context, item: str, item_id: int, feature_id: int
|
|
438
636
|
) -> Iterator[Finding]:
|
|
439
637
|
body = self.text(ctx, item) or ""
|
|
440
|
-
yield from self._common(ctx, item, body, item_id)
|
|
638
|
+
yield from self._common(ctx, item, body, item_id, self.ITEM_STATUSES)
|
|
441
639
|
|
|
442
640
|
declared = self._field(body, self.TYPE_LINE)
|
|
443
641
|
if declared is None:
|
|
@@ -452,3 +650,33 @@ class PlansLayout(Check):
|
|
|
452
650
|
f"type {declared!r} is not pbi or bug",
|
|
453
651
|
f"set type in {item} to pbi (Product Backlog Item) or bug",
|
|
454
652
|
)
|
|
653
|
+
|
|
654
|
+
# R4: the item names its Feature's full board URL; the check requires
|
|
655
|
+
# the key and verifies it against the folder id.
|
|
656
|
+
found_feature, why = PlanMetadata.feature_id(body)
|
|
657
|
+
if why == "missing":
|
|
658
|
+
yield self.report(
|
|
659
|
+
item,
|
|
660
|
+
"missing feature link",
|
|
661
|
+
f"add `feature:` to {item} as the Feature's full board URL "
|
|
662
|
+
f"(.../_workitems/edit/{feature_id})",
|
|
663
|
+
)
|
|
664
|
+
elif why:
|
|
665
|
+
yield self.report(
|
|
666
|
+
item,
|
|
667
|
+
"feature must be a board URL, not a bare id",
|
|
668
|
+
f"set feature in {item} to "
|
|
669
|
+
f"https://dev.azure.com/<org>/<project>/_workitems/edit/{feature_id}",
|
|
670
|
+
)
|
|
671
|
+
elif found_feature != feature_id:
|
|
672
|
+
yield self.report(
|
|
673
|
+
item,
|
|
674
|
+
f"feature {found_feature} disagrees with az{feature_id}",
|
|
675
|
+
f"point feature at {feature_id} in {item}, or move the file "
|
|
676
|
+
f"under az{found_feature}",
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if declared == "bug":
|
|
680
|
+
yield from self._sections(item, body, self.BUG_SECTIONS)
|
|
681
|
+
else:
|
|
682
|
+
yield from self._sections(item, body, self.PBI_SECTIONS)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: design
|
|
3
|
+
title: "Test coverage"
|
|
4
|
+
status: New
|
|
5
|
+
work_item: https://dev.azure.com/org/proj/_workitems/edit/347
|
|
6
|
+
last_update: 2026-09-15T03:20:00Z
|
|
7
|
+
sources:
|
|
8
|
+
- requirements.md
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Test coverage
|
|
12
|
+
|
|
13
|
+
## Solution
|
|
14
|
+
|
|
15
|
+
A coverage job per gate.
|
|
16
|
+
|
|
17
|
+
## Decisions
|
|
18
|
+
|
|
19
|
+
The job runs in CI; the alternative, a local wrapper, was rejected.
|
|
20
|
+
|
|
21
|
+
## Verification
|
|
22
|
+
|
|
23
|
+
AC-1: the suite covering the coverage job.
|
|
24
|
+
|
|
25
|
+
## Docs impact
|
|
26
|
+
|
|
27
|
+
`docs/operations/compliance-controls.md`.
|
|
28
|
+
|
|
29
|
+
## Size estimate
|
|
30
|
+
|
|
31
|
+
One item, two touchpoints, about 40 lines; the job reuses the existing runner.
|
|
32
|
+
|
|
33
|
+
## Risks
|
|
34
|
+
|
|
35
|
+
Flaky timing -- the job pins the runner version.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: file-map
|
|
3
|
+
title: "Test coverage"
|
|
4
|
+
status: New
|
|
5
|
+
work_item: https://dev.azure.com/org/proj/_workitems/edit/347
|
|
6
|
+
last_update: 2026-09-15T03:20:00Z
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Test coverage
|
|
10
|
+
|
|
11
|
+
## Baseline
|
|
12
|
+
|
|
13
|
+
0000000000000000000000000000000000000000
|
|
14
|
+
|
|
15
|
+
## Map
|
|
16
|
+
|
|
17
|
+
| Path | Kind | Why | Items |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| workflows/ | read | the CI layout | 351 |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: intent
|
|
3
|
+
title: "Test coverage"
|
|
4
|
+
status: New
|
|
5
|
+
work_item: https://dev.azure.com/org/proj/_workitems/edit/347
|
|
6
|
+
last_update: 2026-09-15T03:20:00Z
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Test coverage
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Coverage tells us which gates are exercised.
|
|
14
|
+
|
|
15
|
+
## Outcome
|
|
16
|
+
|
|
17
|
+
Every gate has a measured coverage figure.
|
|
18
|
+
|
|
19
|
+
## Constraints
|
|
20
|
+
|
|
21
|
+
None.
|
|
22
|
+
|
|
23
|
+
## Non-goals
|
|
24
|
+
|
|
25
|
+
Branch coverage.
|
|
@@ -3,7 +3,32 @@ type: bug
|
|
|
3
3
|
title: "Restore the entrypoint"
|
|
4
4
|
status: Done
|
|
5
5
|
work_item: https://dev.azure.com/org/proj/_workitems/edit/351
|
|
6
|
+
feature: https://dev.azure.com/org/proj/_workitems/edit/347
|
|
6
7
|
last_update: 2026-09-15T03:20:00Z
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Restore the entrypoint
|
|
11
|
+
|
|
12
|
+
## Current behaviour
|
|
13
|
+
|
|
14
|
+
The entrypoint is gone.
|
|
15
|
+
|
|
16
|
+
## Expected behaviour
|
|
17
|
+
|
|
18
|
+
THE SYSTEM SHALL restore the entrypoint.
|
|
19
|
+
|
|
20
|
+
## Behaviour that must not change
|
|
21
|
+
|
|
22
|
+
The CLI still parses arguments.
|
|
23
|
+
|
|
24
|
+
## Responsible Feature
|
|
25
|
+
|
|
26
|
+
az347 test coverage.
|
|
27
|
+
|
|
28
|
+
## Predecessors
|
|
29
|
+
|
|
30
|
+
none
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
The entrypoint suite passes.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: feature
|
|
3
|
+
title: "Test coverage"
|
|
4
|
+
status: New
|
|
5
|
+
epic: https://dev.azure.com/org/proj/_workitems/edit/320
|
|
6
|
+
work_item: https://dev.azure.com/org/proj/_workitems/edit/347
|
|
7
|
+
last_update: 2026-09-15T03:20:00Z
|
|
8
|
+
sources:
|
|
9
|
+
- intent.md
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Test coverage
|
|
13
|
+
|
|
14
|
+
## Problem
|
|
15
|
+
|
|
16
|
+
Gate coverage is unmeasured.
|
|
17
|
+
|
|
18
|
+
## Acceptance criteria
|
|
19
|
+
|
|
20
|
+
- AC-1: THE SYSTEM SHALL measure coverage per gate.
|
|
21
|
+
|
|
22
|
+
## Scope
|
|
23
|
+
|
|
24
|
+
In: the measurement job. Non-goals: branch coverage.
|
|
25
|
+
|
|
26
|
+
## Predecessors
|
|
27
|
+
|
|
28
|
+
none
|
|
29
|
+
|
|
30
|
+
## Items
|
|
31
|
+
|
|
32
|
+
| Id | Title | Type | Delivers | Verification |
|
|
33
|
+
| --- | --- | --- | --- | --- |
|
|
34
|
+
| 351 | Restore the entrypoint | pbi | AC-1 | suite |
|
|
35
|
+
|
|
36
|
+
## Open questions for G1
|
|
37
|
+
|
|
38
|
+
none
|
|
39
|
+
|
|
40
|
+
## Revision log
|
|
41
|
+
|
|
42
|
+
| Rev | Date | Baseline | Trigger | Changes |
|
|
43
|
+
| --- | --- | --- | --- | --- |
|
|
44
|
+
| 1 | 2026-09-15 | — | G1 | initial |
|