@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
|
@@ -45,7 +45,7 @@ VIOLATING = (
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
# In the corpus, never findings: exempt guide names (root and nested),
|
|
48
|
-
# the standing catalog
|
|
48
|
+
# the standing catalog name, non-Markdown assets (including one
|
|
49
49
|
# with a deliberately violating stem), and a path that merely lookalikes
|
|
50
50
|
# the docs/ prefix.
|
|
51
51
|
NOT_FINDINGS = (
|
|
@@ -54,7 +54,6 @@ NOT_FINDINGS = (
|
|
|
54
54
|
"docs/CLAUDE.md",
|
|
55
55
|
"docs/architecture/AGENTS.md",
|
|
56
56
|
"docs/index.md",
|
|
57
|
-
"docs/notes.md",
|
|
58
57
|
"docs/reference.html",
|
|
59
58
|
"docs/assets/diagram.png",
|
|
60
59
|
"docs/assets/UI-CONTRACT.png",
|
|
@@ -147,12 +146,11 @@ def test_guide_names_are_exempt_at_any_depth() -> None:
|
|
|
147
146
|
assert "docs/architecture/AGENTS.md" not in text
|
|
148
147
|
|
|
149
148
|
|
|
150
|
-
def
|
|
151
|
-
"""`index.md` (the standing catalog name, also scope-excluded)
|
|
152
|
-
|
|
149
|
+
def test_the_standing_catalog_name_is_silent() -> None:
|
|
150
|
+
"""`index.md` (the standing catalog name, also scope-excluded) carries a
|
|
151
|
+
conforming stem and stays silent."""
|
|
153
152
|
text = "\n".join(messages_for(corpus_files(CORPUS)))
|
|
154
153
|
assert "docs/index.md" not in text
|
|
155
|
-
assert "docs/notes.md" not in text
|
|
156
154
|
|
|
157
155
|
|
|
158
156
|
def test_non_markdown_files_are_not_graded() -> None:
|
|
@@ -282,6 +282,21 @@ def test_a_ref_with_an_anchor_onto_a_real_path_passes(tmp_path: pathlib.Path) ->
|
|
|
282
282
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
283
283
|
|
|
284
284
|
|
|
285
|
+
# ---------------------------------------------------------------------- R9 (§4a)
|
|
286
|
+
|
|
287
|
+
def test_a_dispatch_order_table_beside_the_event_table_is_not_graded(
|
|
288
|
+
tmp_path: pathlib.Path,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""R9: the LEDGER holds the event table only. Order comes from Feature
|
|
291
|
+
predecessor links on the board, so a leftover `## Next` table -- any table
|
|
292
|
+
whose columns are not the event columns -- is not ledger content and must
|
|
293
|
+
not be graded, reported or required."""
|
|
294
|
+
body = table("| 2026-01-01 | an-entry | docs | something happened | — |")
|
|
295
|
+
body += "\n## Next\n\n| Order | Feature |\n| --- | --- |\n| 1 | az999-next |\n"
|
|
296
|
+
verdict = run_body(tmp_path, body)
|
|
297
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
298
|
+
|
|
299
|
+
|
|
285
300
|
# -------------------------------------------------------------- the inverted half
|
|
286
301
|
|
|
287
302
|
|
|
@@ -42,7 +42,7 @@ common_optional:
|
|
|
42
42
|
- last_update
|
|
43
43
|
- related
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# Four profiles, deliberately (Compliance is disabled, see below).
|
|
46
46
|
#
|
|
47
47
|
# There were eight. Benchmark, Evaluation, Field Note and Specification declared
|
|
48
48
|
# trees that do not exist -- docs/research/benchmarks, docs/research/evaluations,
|
|
@@ -64,7 +64,6 @@ common_optional:
|
|
|
64
64
|
# Architecture what the system is docs/architecture/**
|
|
65
65
|
# ADR why it is that way docs/architecture/decisions/**, docs/decisions/**
|
|
66
66
|
# Operations how it is operated docs/operations/**
|
|
67
|
-
# Task ledger what each agent task did docs/notes.md
|
|
68
67
|
# Research what is being found out docs/research/**
|
|
69
68
|
# (Compliance what we must show docs/compliance/** -- disabled, see below)
|
|
70
69
|
#
|
|
@@ -142,21 +141,6 @@ profiles:
|
|
|
142
141
|
- ["Credentials", "Access", "Sign"]
|
|
143
142
|
- ["Verification", "Evidence", "Check"]
|
|
144
143
|
|
|
145
|
-
# The task ledger: one append-only list of dated lines, one per task start
|
|
146
|
-
# and finish, that the global agent rule requires in every repository
|
|
147
|
-
# (docs/notes.md, declared in policy/docs_shape.py). It is an operations
|
|
148
|
-
# document, but not a procedure: it has no Purpose/Procedure/Credentials
|
|
149
|
-
# sections and no `related` field, so it gets its own profile rather than
|
|
150
|
-
# the Operations one above. It is graded for what it really has: OKF
|
|
151
|
-
# frontmatter with `sources`, and its `## Lines` section.
|
|
152
|
-
- type: Operations
|
|
153
|
-
paths:
|
|
154
|
-
- docs/notes.md
|
|
155
|
-
required:
|
|
156
|
-
- sources
|
|
157
|
-
headings:
|
|
158
|
-
- ["Lines"]
|
|
159
|
-
|
|
160
144
|
# Research: inputs, findings and experiment plans under docs/research/. No
|
|
161
145
|
# `type` -- the pages there differ (Research, Experiment) -- and no headings;
|
|
162
146
|
# see the header. Title, description and status come from common_required.
|
|
@@ -5,9 +5,9 @@ Architecture page, and one with an invalid `status` value. Each corpus carries
|
|
|
5
5
|
its own cut-down profile register, passed as the `profiles` input override —
|
|
6
6
|
the packaged production register is not what these two scenarios are about.
|
|
7
7
|
|
|
8
|
-
The tests below them grade the PACKAGED register: the
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The tests below them grade the PACKAGED register: the Research profile for
|
|
9
|
+
`docs/research/**` together with the engine rule it needs (a profile may
|
|
10
|
+
declare no `type`, leaving the value unconstrained).
|
|
11
11
|
|
|
12
12
|
The last section grades the closed key set (2026-10-02): a document may carry
|
|
13
13
|
only the keys its profile admits — `common_required`, `common_optional`, and
|
|
@@ -72,25 +72,6 @@ def run_packaged(case: str, only: tuple[str, ...] | None = None) -> object:
|
|
|
72
72
|
return runner.run(ROW, directory, given)
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def test_the_task_ledger_matches_a_packaged_profile() -> None:
|
|
76
|
-
"""`docs/notes.md` is the repository's task ledger (the global agent rule:
|
|
77
|
-
one line at the start and one at the finish of every task). It carries
|
|
78
|
-
OKF frontmatter and a `## Lines` section and nothing else a profile could
|
|
79
|
-
demand, so the packaged register must select a profile for it."""
|
|
80
|
-
verdict = run_packaged("okf-ledger-clean")
|
|
81
|
-
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_the_task_ledger_profile_requires_the_lines_section() -> None:
|
|
85
|
-
"""Without this, the clean case above also passes against a profile that
|
|
86
|
-
asserts nothing about the ledger."""
|
|
87
|
-
verdict = run_packaged("okf-ledger-no-lines")
|
|
88
|
-
assert verdict.outcome is Outcome.FINDINGS
|
|
89
|
-
assert any("missing one of headings: Lines" in f.message for f in verdict.findings), (
|
|
90
|
-
verdict.render()
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
75
|
def messages(verdict: object, relative: str) -> list[str]:
|
|
95
76
|
"""Every message reported against one file, sorted, so a test can assert the
|
|
96
77
|
EXACT set rather than that some finding somewhere mentioned a field."""
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
"""LAY-016 — at most ten open plans
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
repository declares its own, as
|
|
9
|
-
open-plans=N` (`[controls.compliance.limits].
|
|
10
|
-
adl-config.toml). `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
"""LAY-016 — at most ten open plans, by default.
|
|
2
|
+
|
|
3
|
+
Open plans use the Feature board-state vocabulary shared with LAY-009 (R1):
|
|
4
|
+
a Feature folder's `intent.md` is the one plan file that exists from the
|
|
5
|
+
Feature's first day (R2), so it is the state this cap counts. New, In Progress
|
|
6
|
+
and Awaiting Release are open; Done and Removed are closed.
|
|
7
|
+
|
|
8
|
+
The cap is `OPEN_LIMIT` (10) unless the repository declares its own, as
|
|
9
|
+
`--declare open-plans=N` (`[controls.compliance.limits].open_plans` in its
|
|
10
|
+
adl-config.toml). `open_plans_limit` resolves it: absent is the default, and a
|
|
11
|
+
value that is not a base-10 positive integer is could-not-run, never a quiet
|
|
12
|
+
fall back to the default. The engine default is not raised; a repository
|
|
13
|
+
states the count it carries today and ratchets it down.
|
|
14
|
+
|
|
15
|
+
R7 (ADL process handover, 2026-10-12) retired the register half of this gate
|
|
16
|
+
with `plans/GAPS-AND-FINDINGS.md` itself: the open-gaps cap, the register
|
|
17
|
+
columns and the `open-gaps` declaration are gone. The open-plans cap stays.
|
|
15
18
|
"""
|
|
16
19
|
|
|
17
20
|
from __future__ import annotations
|
|
@@ -33,63 +36,30 @@ class OpenWorkCap(Check):
|
|
|
33
36
|
inputs: ClassVar[dict[str, str]] = {"plans_root": "<plans>"}
|
|
34
37
|
whole_scope: ClassVar[bool] = True
|
|
35
38
|
|
|
36
|
-
# The engine default
|
|
37
|
-
# `open-
|
|
39
|
+
# The engine default. A repository states its own with the
|
|
40
|
+
# `open-plans` declaration; see `open_plans_limit`.
|
|
38
41
|
OPEN_LIMIT: ClassVar[int] = 10
|
|
39
|
-
OPEN_GAPS_DECLARATION: ClassVar[str] = "open-gaps"
|
|
40
42
|
OPEN_PLANS_DECLARATION: ClassVar[str] = "open-plans"
|
|
41
43
|
FEATURES: ClassVar[str] = "features"
|
|
42
|
-
|
|
43
|
-
REGISTER: ClassVar[str] = "GAPS-AND-FINDINGS.md"
|
|
44
|
+
INTENT: ClassVar[str] = "intent.md"
|
|
44
45
|
IGNORED: ClassVar[str] = ".DS_Store"
|
|
45
|
-
COLUMNS: ClassVar[tuple[str, ...]] = (
|
|
46
|
-
"Found",
|
|
47
|
-
"Finding",
|
|
48
|
-
"From plan",
|
|
49
|
-
"Item file",
|
|
50
|
-
"Work item",
|
|
51
|
-
)
|
|
52
|
-
DIVIDER_CELL: ClassVar[re.Pattern[str]] = re.compile(r"^:?-{3,}:?$")
|
|
53
|
-
WORK_ITEM_LINK: ClassVar[re.Pattern[str]] = re.compile(
|
|
54
|
-
r"^\[([0-9]+)\]"
|
|
55
|
-
r"\((https://dev\.azure\.com/\S+/_workitems/edit/([0-9]+)/?)\)$"
|
|
56
|
-
)
|
|
57
|
-
UNRAISED: ClassVar[frozenset[str]] = frozenset(("", "-", "—", "TBD"))
|
|
58
|
-
EXCERPT: ClassVar[int] = 60
|
|
59
|
-
|
|
60
|
-
def open_gaps_limit(self, ctx: Context) -> int:
|
|
61
|
-
"""The open-gap cap in force: the declared one, else `OPEN_LIMIT`."""
|
|
62
|
-
return self._declared_limit(ctx, self.OPEN_GAPS_DECLARATION, "open_gaps")
|
|
63
46
|
|
|
64
47
|
def open_plans_limit(self, ctx: Context) -> int:
|
|
65
48
|
"""The open-plan cap in force: the declared one, else `OPEN_LIMIT`."""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _declared_limit(self, ctx: Context, declaration: str, key: str) -> int:
|
|
69
|
-
"""One cap's declared value, else `OPEN_LIMIT`.
|
|
70
|
-
|
|
71
|
-
ASCII digits only, so `+20`, `1_0`, ` 20` and full-width digits are
|
|
72
|
-
refused, which `int()` alone would accept. A declaration that cannot be
|
|
73
|
-
read as a positive integer is could-not-run: falling back to the default
|
|
74
|
-
would grade the repository against a cap it explicitly replaced.
|
|
75
|
-
"""
|
|
76
|
-
declared = ctx.declarations.get(declaration)
|
|
49
|
+
declared = ctx.declarations.get(self.OPEN_PLANS_DECLARATION)
|
|
77
50
|
if declared is None:
|
|
78
51
|
return self.OPEN_LIMIT
|
|
79
52
|
if re.fullmatch(r"[0-9]+", declared) is None or int(declared) < 1:
|
|
80
53
|
raise CouldNotRun(
|
|
81
|
-
f"declaration {
|
|
82
|
-
|
|
54
|
+
f"declaration {self.OPEN_PLANS_DECLARATION}={declared!r} is not a "
|
|
55
|
+
"base-10 positive integer; set "
|
|
56
|
+
"[controls.compliance.limits].open_plans to a whole number "
|
|
83
57
|
"of at least 1"
|
|
84
58
|
)
|
|
85
59
|
return int(declared)
|
|
86
60
|
|
|
87
61
|
def graded(self, ctx: Context, files: FileSet) -> Iterable[Finding]:
|
|
88
|
-
# Both caps are resolved before either is graded, so an unreadable
|
|
89
|
-
# declaration is could-not-run even when the cap it names is not the
|
|
90
|
-
# one being exceeded.
|
|
91
62
|
plans_limit = self.open_plans_limit(ctx)
|
|
92
|
-
gaps_limit = self.open_gaps_limit(ctx)
|
|
93
63
|
|
|
94
64
|
yield from self._cap(
|
|
95
65
|
"plans",
|
|
@@ -99,17 +69,6 @@ class OpenWorkCap(Check):
|
|
|
99
69
|
f"finish or remove open Features until no more than {plans_limit} remain",
|
|
100
70
|
)
|
|
101
71
|
|
|
102
|
-
open_gaps, stale_gaps = self._gaps(ctx, set(files))
|
|
103
|
-
yield from stale_gaps
|
|
104
|
-
yield from self._cap(
|
|
105
|
-
"gaps",
|
|
106
|
-
f"{self._plans(ctx)}/{self.REGISTER}",
|
|
107
|
-
open_gaps,
|
|
108
|
-
gaps_limit,
|
|
109
|
-
"finish linked work and delete its gap row, or resolve unraised gaps, "
|
|
110
|
-
f"until no more than {gaps_limit} remain",
|
|
111
|
-
)
|
|
112
|
-
|
|
113
72
|
def _cap(
|
|
114
73
|
self, subject: str, path: str, open_items: list[str], limit: int, fix: str
|
|
115
74
|
) -> Iterator[Finding]:
|
|
@@ -137,135 +96,33 @@ class OpenWorkCap(Check):
|
|
|
137
96
|
for name in sorted(names):
|
|
138
97
|
if name == self.IGNORED or not ctx.path(f"{features}/{name}").is_dir():
|
|
139
98
|
continue
|
|
140
|
-
|
|
141
|
-
if
|
|
99
|
+
intent = f"{features}/{name}/{self.INTENT}"
|
|
100
|
+
if not ctx.path(intent).is_file():
|
|
101
|
+
# A Feature whose intent.md does not exist yet is mid-migration;
|
|
102
|
+
# LAY-009 reports the missing file. Counting it either way would
|
|
103
|
+
# grade a state nobody established.
|
|
104
|
+
continue
|
|
105
|
+
if intent not in given:
|
|
142
106
|
raise CouldNotRun(
|
|
143
|
-
f"{
|
|
144
|
-
"
|
|
107
|
+
f"{intent} is not in the widened Git-visible file set; an on-disk "
|
|
108
|
+
"intent outside that set cannot establish plan state"
|
|
145
109
|
)
|
|
146
|
-
body = self.text(ctx,
|
|
110
|
+
body = self.text(ctx, intent)
|
|
147
111
|
if body is None:
|
|
148
112
|
raise CouldNotRun(
|
|
149
|
-
f"{
|
|
113
|
+
f"{intent} is missing; an unreadable plan state cannot be counted"
|
|
150
114
|
)
|
|
151
|
-
status = self._status(body,
|
|
152
|
-
if status in PlanMetadata.
|
|
115
|
+
status = self._status(body, intent)
|
|
116
|
+
if status in PlanMetadata.FEATURE_ACTIVE_STATUSES:
|
|
153
117
|
open_plans.append(name)
|
|
154
118
|
return open_plans
|
|
155
119
|
|
|
156
120
|
def _status(self, body: str, path: str) -> str:
|
|
157
121
|
status = PlanMetadata.field(body, PlanMetadata.STATUS_LINE)
|
|
158
|
-
if status not in PlanMetadata.
|
|
122
|
+
if status not in PlanMetadata.FEATURE_STATUSES:
|
|
159
123
|
shown = "missing" if status is None else repr(status)
|
|
160
124
|
raise CouldNotRun(
|
|
161
125
|
f"{path} has {shown} status; expected an exact work item state: "
|
|
162
|
-
f"{', '.join(PlanMetadata.
|
|
126
|
+
f"{', '.join(PlanMetadata.FEATURE_STATUSES)}"
|
|
163
127
|
)
|
|
164
128
|
return status
|
|
165
|
-
|
|
166
|
-
def _gaps(self, ctx: Context, given: set[str]) -> tuple[list[str], list[Finding]]:
|
|
167
|
-
register = f"{self._plans(ctx)}/{self.REGISTER}"
|
|
168
|
-
if register not in given:
|
|
169
|
-
return [], []
|
|
170
|
-
body = self.text(ctx, register)
|
|
171
|
-
if body is None:
|
|
172
|
-
raise CouldNotRun(
|
|
173
|
-
f"{register} is in the widened Git-visible file set but cannot be read"
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
rows = [
|
|
177
|
-
(number, self._cells(line))
|
|
178
|
-
for number, line in enumerate(body.splitlines(), start=1)
|
|
179
|
-
if line.lstrip().startswith("|")
|
|
180
|
-
]
|
|
181
|
-
if not rows:
|
|
182
|
-
raise CouldNotRun(f"{register} exists but holds no Markdown table")
|
|
183
|
-
if tuple(rows[0][1]) != self.COLUMNS:
|
|
184
|
-
raise CouldNotRun(
|
|
185
|
-
f"{register} has columns {rows[0][1]}, expected {list(self.COLUMNS)}"
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
open_gaps: list[str] = []
|
|
189
|
-
stale: list[Finding] = []
|
|
190
|
-
for number, cells in rows[1:]:
|
|
191
|
-
if len(cells) != len(self.COLUMNS):
|
|
192
|
-
raise CouldNotRun(
|
|
193
|
-
f"{register}:{number} has {len(cells)} cells; expected "
|
|
194
|
-
f"{len(self.COLUMNS)}, so its work-item column cannot be identified"
|
|
195
|
-
)
|
|
196
|
-
if all(self.DIVIDER_CELL.match(cell) for cell in cells):
|
|
197
|
-
continue
|
|
198
|
-
|
|
199
|
-
label = f"line {number}: {cells[0]} {self._excerpt(cells[1])}"
|
|
200
|
-
work_item_cell = self._plain(cells[4])
|
|
201
|
-
if work_item_cell in self.UNRAISED:
|
|
202
|
-
open_gaps.append(label)
|
|
203
|
-
continue
|
|
204
|
-
|
|
205
|
-
linked = self.WORK_ITEM_LINK.match(work_item_cell)
|
|
206
|
-
if linked is None or linked.group(1) != linked.group(3):
|
|
207
|
-
raise CouldNotRun(
|
|
208
|
-
f"{register}:{number} has malformed work item link "
|
|
209
|
-
f"{work_item_cell!r}; use [id](https://dev.azure.com/.../_workitems/edit/id)"
|
|
210
|
-
)
|
|
211
|
-
linked_id = int(linked.group(1))
|
|
212
|
-
item_path = self._plain(cells[3])
|
|
213
|
-
item_match = re.fullmatch(
|
|
214
|
-
rf"{re.escape(self._plans(ctx))}/{self.FEATURES}/"
|
|
215
|
-
r"az[0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*/items/"
|
|
216
|
-
r"az([0-9]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md",
|
|
217
|
-
item_path,
|
|
218
|
-
)
|
|
219
|
-
if item_match is None or int(item_match.group(1)) != linked_id:
|
|
220
|
-
raise CouldNotRun(
|
|
221
|
-
f"{register}:{number} item file {item_path!r} must be a normalized "
|
|
222
|
-
f"repository-relative {self._plans(ctx)}/features/az<feature>-<slug>/"
|
|
223
|
-
f"items/az{linked_id}-<slug>.md path"
|
|
224
|
-
)
|
|
225
|
-
if item_path not in given:
|
|
226
|
-
raise CouldNotRun(
|
|
227
|
-
f"{register}:{number} item file {item_path!r} is not in the widened "
|
|
228
|
-
"Git-visible file set; an on-disk path outside that set cannot "
|
|
229
|
-
"establish linked work state"
|
|
230
|
-
)
|
|
231
|
-
item_body = self.text(ctx, item_path)
|
|
232
|
-
if item_body is None:
|
|
233
|
-
raise CouldNotRun(
|
|
234
|
-
f"{register}:{number} names item file {item_path!r}, but it is missing"
|
|
235
|
-
)
|
|
236
|
-
found_id, why = PlanMetadata.work_item_id(item_body)
|
|
237
|
-
if why or found_id != linked_id:
|
|
238
|
-
raise CouldNotRun(
|
|
239
|
-
f"{item_path} must carry a work_item link for {linked_id}; "
|
|
240
|
-
f"found {why or found_id}"
|
|
241
|
-
)
|
|
242
|
-
status = self._status(item_body, item_path)
|
|
243
|
-
if status in PlanMetadata.ACTIVE_STATUSES:
|
|
244
|
-
open_gaps.append(label)
|
|
245
|
-
elif status in PlanMetadata.TERMINAL_STATUSES:
|
|
246
|
-
stale.append(
|
|
247
|
-
self.report(
|
|
248
|
-
register,
|
|
249
|
-
f"closed gap remains in the register at {label}; linked item "
|
|
250
|
-
f"{linked_id} is {status}",
|
|
251
|
-
f"delete line {number} from {register}; closed gaps no longer remain",
|
|
252
|
-
line=number,
|
|
253
|
-
)
|
|
254
|
-
)
|
|
255
|
-
return open_gaps, stale
|
|
256
|
-
|
|
257
|
-
@staticmethod
|
|
258
|
-
def _plain(value: str) -> str:
|
|
259
|
-
stripped = value.strip()
|
|
260
|
-
if len(stripped) >= 2 and stripped.startswith("`") and stripped.endswith("`"):
|
|
261
|
-
return stripped[1:-1].strip()
|
|
262
|
-
return stripped
|
|
263
|
-
|
|
264
|
-
@staticmethod
|
|
265
|
-
def _cells(line: str) -> list[str]:
|
|
266
|
-
inner = line.strip().removeprefix("|").removesuffix("|")
|
|
267
|
-
return [cell.strip().replace(r"\|", "|") for cell in re.split(r"(?<!\\)\|", inner)]
|
|
268
|
-
|
|
269
|
-
def _excerpt(self, value: str) -> str:
|
|
270
|
-
flat = " ".join(value.split())
|
|
271
|
-
return flat if len(flat) <= self.EXCERPT else f"{flat[: self.EXCERPT]}…"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: intent
|
|
3
|
+
title: "A clean feature"
|
|
4
|
+
status: New
|
|
5
|
+
work_item: https://dev.azure.com/org/project/_workitems/edit/601
|
|
6
|
+
last_update: 2026-09-17T00:00:00Z
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# A clean feature
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
## Outcome
|
|
14
|
+
|
|
15
|
+
## Constraints
|
|
16
|
+
|
|
17
|
+
## Non-goals
|