@deftai/directive-content 0.55.2 → 0.56.0
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/.githooks/pre-commit +143 -0
- package/.githooks/pre-push +121 -0
- package/QUICK-START.md +2 -2
- package/Taskfile.yml +934 -0
- package/UPGRADING.md +47 -1
- package/events/README.md +3 -3
- package/package.json +5 -4
- package/scripts/_agents_md.py +494 -0
- package/scripts/_cache_fetch.py +635 -0
- package/scripts/_cache_quota.py +529 -0
- package/scripts/_cache_refresh.py +163 -0
- package/scripts/_cache_validate.py +209 -0
- package/scripts/_content_root.py +42 -0
- package/scripts/_doctor_state.py +277 -0
- package/scripts/_event_detect.py +305 -0
- package/scripts/_events.py +514 -0
- package/scripts/_lifecycle_hygiene.py +568 -0
- package/scripts/_pathspec.py +91 -0
- package/scripts/_policy_show_cli.py +266 -0
- package/scripts/_precutover.py +92 -0
- package/scripts/_project_context.py +224 -0
- package/scripts/_project_definition_io.py +164 -0
- package/scripts/_relocate_snapshot.py +209 -0
- package/scripts/_relocate_states.py +343 -0
- package/scripts/_resolve_preflight_path.py +152 -0
- package/scripts/_safe_subprocess.py +167 -0
- package/scripts/_session_start_hook.py +205 -0
- package/scripts/_sor_gate_diff.py +365 -0
- package/scripts/_stdio_utf8.py +59 -0
- package/scripts/_triage_bootstrap_gitignore.py +904 -0
- package/scripts/_triage_classify_cli.py +122 -0
- package/scripts/_triage_queue_cli.py +625 -0
- package/scripts/_triage_scope_cli.py +343 -0
- package/scripts/_triage_scope_drift_cli.py +121 -0
- package/scripts/_triage_scope_ignores.py +286 -0
- package/scripts/_triage_scope_milestone.py +432 -0
- package/scripts/_triage_scope_mutations.py +337 -0
- package/scripts/_triage_scope_renderers.py +207 -0
- package/scripts/_triage_smoketest_stages.py +674 -0
- package/scripts/_triage_subscribe_cli.py +140 -0
- package/scripts/_triage_welcome_cli.py +421 -0
- package/scripts/_vbrief_build.py +239 -0
- package/scripts/_vbrief_fidelity.py +479 -0
- package/scripts/_vbrief_legacy.py +589 -0
- package/scripts/_vbrief_reconciliation.py +883 -0
- package/scripts/_vbrief_routing.py +277 -0
- package/scripts/_vbrief_safety.py +778 -0
- package/scripts/_vbrief_sources.py +312 -0
- package/scripts/_vbrief_speckit.py +262 -0
- package/scripts/_vbrief_story_quality.py +353 -0
- package/scripts/_vbrief_validation.py +299 -0
- package/scripts/build_dist.py +412 -0
- package/scripts/cache.py +1078 -0
- package/scripts/cache_scanner.py +745 -0
- package/scripts/candidates_log.py +432 -0
- package/scripts/capacity_backfill.py +680 -0
- package/scripts/capacity_show.py +653 -0
- package/scripts/ci_local.py +689 -0
- package/scripts/code_structure_validate.py +765 -0
- package/scripts/codebase_default_extractor.py +495 -0
- package/scripts/codebase_map.py +304 -0
- package/scripts/codebase_map_fresh.py +104 -0
- package/scripts/codebase_projection_registry.py +94 -0
- package/scripts/codebase_provider.py +582 -0
- package/scripts/doctor.py +2257 -0
- package/scripts/framework_commands.py +505 -0
- package/scripts/gh_rest.py +882 -0
- package/scripts/github_auth_modes.py +437 -0
- package/scripts/github_body.py +292 -0
- package/scripts/ip_risk.py +531 -0
- package/scripts/issue_emit.py +670 -0
- package/scripts/issue_ingest.py +1064 -0
- package/scripts/migrate_preflight.py +418 -0
- package/scripts/migrate_vbrief.py +2677 -0
- package/scripts/monitor_pr.py +401 -0
- package/scripts/pack_migrate_lessons.py +336 -0
- package/scripts/pack_migrate_patterns.py +254 -0
- package/scripts/pack_migrate_rules.py +350 -0
- package/scripts/pack_migrate_skills.py +423 -0
- package/scripts/pack_migrate_strategies.py +311 -0
- package/scripts/pack_migrate_swarm_spec.py +250 -0
- package/scripts/pack_render.py +434 -0
- package/scripts/packs_slice.py +712 -0
- package/scripts/platform_capabilities.py +336 -0
- package/scripts/policy.py +2826 -0
- package/scripts/policy_set.py +324 -0
- package/scripts/pr_check_closing_keywords.py +524 -0
- package/scripts/pr_check_protected_issues.py +267 -0
- package/scripts/pr_merge_readiness.py +1004 -0
- package/scripts/pr_wait_mergeable.py +669 -0
- package/scripts/prd_render.py +159 -0
- package/scripts/preflight_architecture_sor.py +974 -0
- package/scripts/preflight_branch.py +289 -0
- package/scripts/preflight_cache.py +974 -0
- package/scripts/preflight_gh.py +721 -0
- package/scripts/preflight_implementation.py +272 -0
- package/scripts/preflight_story_start.py +838 -0
- package/scripts/preflight_wip_cap.py +149 -0
- package/scripts/probe_session.py +545 -0
- package/scripts/project_render.py +293 -0
- package/scripts/quarantine_ext.py +237 -0
- package/scripts/reconcile_issues.py +1442 -0
- package/scripts/refresh-path.ps1 +107 -0
- package/scripts/release.py +2030 -0
- package/scripts/release_e2e.py +1011 -0
- package/scripts/release_publish.py +486 -0
- package/scripts/release_rollback.py +980 -0
- package/scripts/relocate.py +1034 -0
- package/scripts/resolve_changelog_unreleased.py +667 -0
- package/scripts/resolve_version.py +490 -0
- package/scripts/resume_conditions.py +706 -0
- package/scripts/ritual_sentinel.py +609 -0
- package/scripts/roadmap_render.py +635 -0
- package/scripts/rule_ownership_lint.py +325 -0
- package/scripts/scm.py +591 -0
- package/scripts/scope_audit_log.py +387 -0
- package/scripts/scope_decompose.py +654 -0
- package/scripts/scope_demote.py +509 -0
- package/scripts/scope_lifecycle.py +1126 -0
- package/scripts/scope_undo.py +772 -0
- package/scripts/session_start.py +406 -0
- package/scripts/setup_ghx.py +339 -0
- package/scripts/setup_windows.ps1 +220 -0
- package/scripts/slice_audit.py +585 -0
- package/scripts/slice_record.py +530 -0
- package/scripts/slice_record_existing.py +692 -0
- package/scripts/slug_normalize.py +178 -0
- package/scripts/spec_render.py +477 -0
- package/scripts/spec_validate.py +238 -0
- package/scripts/subagent_monitor.py +658 -0
- package/scripts/swarm_complete_cohort.py +644 -0
- package/scripts/swarm_launch.py +1206 -0
- package/scripts/swarm_readiness.py +554 -0
- package/scripts/swarm_verify_review_clean.py +438 -0
- package/scripts/swarm_worktrees.py +497 -0
- package/scripts/toolchain-check.py +52 -0
- package/scripts/triage_actions.py +871 -0
- package/scripts/triage_bootstrap.py +1153 -0
- package/scripts/triage_bulk.py +630 -0
- package/scripts/triage_classify.py +932 -0
- package/scripts/triage_help.py +1685 -0
- package/scripts/triage_queue.py +1944 -0
- package/scripts/triage_reconcile.py +581 -0
- package/scripts/triage_refresh.py +643 -0
- package/scripts/triage_scope.py +999 -0
- package/scripts/triage_scope_drift.py +575 -0
- package/scripts/triage_smoketest.py +396 -0
- package/scripts/triage_subscribe.py +399 -0
- package/scripts/triage_summary.py +1011 -0
- package/scripts/triage_welcome.py +1178 -0
- package/scripts/ts_check_lane.py +86 -0
- package/scripts/validate-links.py +64 -0
- package/scripts/validate_strategy_output.py +212 -0
- package/scripts/vbrief_activate.py +228 -0
- package/scripts/vbrief_migrate_conformance.py +368 -0
- package/scripts/vbrief_reconcile_graph.py +306 -0
- package/scripts/vbrief_reconcile_labels.py +460 -0
- package/scripts/vbrief_reconcile_umbrellas.py +741 -0
- package/scripts/vbrief_validate.py +1195 -0
- package/scripts/verify-stubs.py +61 -0
- package/scripts/verify_capacity.py +160 -0
- package/scripts/verify_encoding.py +699 -0
- package/scripts/verify_hooks_installed.py +206 -0
- package/scripts/verify_investigation.py +360 -0
- package/scripts/verify_judgment_gates.py +827 -0
- package/scripts/verify_no_task_runtime.py +171 -0
- package/scripts/verify_scm_boundary.py +509 -0
- package/scripts/verify_session_ritual.py +389 -0
- package/scripts/verify_tools.py +426 -0
- package/scripts/verify_vbrief_conformance.py +478 -0
- package/tasks/architecture.yml +13 -0
- package/tasks/cache.yml +69 -0
- package/tasks/capacity.yml +38 -0
- package/tasks/change.yml +46 -0
- package/tasks/changelog.yml +24 -0
- package/tasks/ci.yml +49 -0
- package/tasks/codebase.yml +47 -0
- package/tasks/commit.yml +30 -0
- package/tasks/core.yml +126 -0
- package/tasks/deployments.yml +54 -0
- package/tasks/framework.yml +74 -0
- package/tasks/install.yml +60 -0
- package/tasks/issue.yml +50 -0
- package/tasks/migrate.yml +73 -0
- package/tasks/packs.yml +92 -0
- package/tasks/policy.yml +75 -0
- package/tasks/pr.yml +89 -0
- package/tasks/prd.yml +39 -0
- package/tasks/project.yml +27 -0
- package/tasks/reconcile.yml +32 -0
- package/tasks/relocate.yml +56 -0
- package/tasks/roadmap.yml +28 -0
- package/tasks/scm.yml +126 -0
- package/tasks/scope-undo.yml +36 -0
- package/tasks/scope.yml +141 -0
- package/tasks/session.yml +19 -0
- package/tasks/setup.yml +37 -0
- package/tasks/slice.yml +69 -0
- package/tasks/spec.yml +41 -0
- package/tasks/swarm.yml +85 -0
- package/tasks/toolchain.yml +13 -0
- package/tasks/triage-actions.yml +94 -0
- package/tasks/triage-bootstrap.yml +43 -0
- package/tasks/triage-bulk.yml +75 -0
- package/tasks/triage-classify.yml +30 -0
- package/tasks/triage-queue.yml +50 -0
- package/tasks/triage-reconcile.yml +29 -0
- package/tasks/triage-scope-drift.yml +29 -0
- package/tasks/triage-scope.yml +31 -0
- package/tasks/triage-smoketest.yml +33 -0
- package/tasks/triage-subscribe.yml +36 -0
- package/tasks/triage-summary.yml +29 -0
- package/tasks/triage-welcome.yml +32 -0
- package/tasks/ts.yml +328 -0
- package/tasks/vbrief.yml +206 -0
- package/tasks/verify.yml +292 -0
- package/templates/agents-entry.md +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""_vbrief_fidelity.py -- Fidelity fixes for migrate:vbrief (#495).
|
|
2
|
+
|
|
3
|
+
Implements the #506 D2/D3/D4 in-scope findings for migrate:vbrief:
|
|
4
|
+
|
|
5
|
+
495-1 Preserve per-task body, ``Depends on:``, and acceptance-criteria
|
|
6
|
+
bullets into scope-vBRIEF narratives (Description / DependsOn /
|
|
7
|
+
AcceptanceCriteria). Body source is SPEC.md per D2 #14 (body
|
|
8
|
+
routing is reconciled by Agent B's reconciliation module; this
|
|
9
|
+
module consumes the reconciled state once available and falls
|
|
10
|
+
back to direct SPEC parsing when running against the baseline).
|
|
11
|
+
495-3 Pass FR-N / NFR-N trace IDs through verbatim -- never renumber.
|
|
12
|
+
495-4 Parse FR-N: / NFR-N: definitions from SPECIFICATION.md and emit
|
|
13
|
+
the ``Requirements`` narrative on ``specification.vbrief.json``.
|
|
14
|
+
495-6 Emit ``plan.edges[]`` from per-task ``Depends on:`` lines (edge
|
|
15
|
+
type = ``blocks``).
|
|
16
|
+
495-6b Fold ``Acceptance Criteria (Project-Level)`` into
|
|
17
|
+
``SuccessMetrics`` (handled by _vbrief_legacy's known-mappings).
|
|
18
|
+
495-9 Align narrative keys to the #506 D3 canonical set per file.
|
|
19
|
+
Fix known PROJECT-DEFINITION bugs (lowercase-space ``tech stack``
|
|
20
|
+
-> PascalCase ``TechStack``; emit DeftVersion, vBRIEFInfo.author,
|
|
21
|
+
vBRIEFInfo.created; ``plan.title`` = project name).
|
|
22
|
+
495-15 Log every narrative routing decision with source file + line
|
|
23
|
+
range + target key + target file so the migrator log is
|
|
24
|
+
unambiguous.
|
|
25
|
+
|
|
26
|
+
Canonical heading -> narrative-key resolution is shared with #505 via
|
|
27
|
+
``_vbrief_legacy`` (single source of truth for the known-mappings list).
|
|
28
|
+
|
|
29
|
+
Issue: #495, #506 D2/D3/D4.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from collections.abc import Iterable
|
|
36
|
+
|
|
37
|
+
# Local imports -- this module lives alongside _vbrief_legacy in scripts/.
|
|
38
|
+
try:
|
|
39
|
+
from _vbrief_legacy import (
|
|
40
|
+
CANONICAL_SPEC_KEYS,
|
|
41
|
+
SPEC_KNOWN_MAPPINGS,
|
|
42
|
+
lookup_canonical,
|
|
43
|
+
parse_top_level_sections,
|
|
44
|
+
partition_sections,
|
|
45
|
+
)
|
|
46
|
+
except ImportError: # pragma: no cover -- imported as package in tests
|
|
47
|
+
from ._vbrief_legacy import ( # type: ignore[no-redef]
|
|
48
|
+
CANONICAL_SPEC_KEYS,
|
|
49
|
+
SPEC_KNOWN_MAPPINGS,
|
|
50
|
+
lookup_canonical,
|
|
51
|
+
parse_top_level_sections,
|
|
52
|
+
partition_sections,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Per-task body parsing (495-1)
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Task headings in SPECIFICATION.md look like:
|
|
61
|
+
#
|
|
62
|
+
# ### tX.Y.Z -- Title [status]
|
|
63
|
+
# ### `tX.Y.Z` Title
|
|
64
|
+
# #### tX.Y.Z Title
|
|
65
|
+
#
|
|
66
|
+
# We keep the match loose so pre-v0.20 spec styles all parse.
|
|
67
|
+
_TASK_HEADING_RE = re.compile(
|
|
68
|
+
r"^(?P<hashes>#{3,4})\s+"
|
|
69
|
+
r"(?:`)?(?P<task_id>t[0-9]+(?:\.[0-9]+)+)(?:`)?"
|
|
70
|
+
r"(?:\s*[-:]+\s*|\s+)"
|
|
71
|
+
r"(?P<title>[^\[\n]+?)"
|
|
72
|
+
r"(?:\s*\[(?P<status>[a-zA-Z_-]+)\])?\s*$"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Recognised "Depends on:" / "DependsOn:" prose lines under a task heading.
|
|
76
|
+
_DEPENDS_ON_RE = re.compile(
|
|
77
|
+
r"^\*{0,2}\s*Depends\s*on\s*\*{0,2}\s*:\s*(?P<deps>.+)$",
|
|
78
|
+
re.IGNORECASE,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Recognised "Traces:" / "**Traces**:" prose lines under a task heading.
|
|
82
|
+
_TRACES_RE = re.compile(
|
|
83
|
+
r"^\s*\*{0,2}\s*Traces\s*\*{0,2}\s*:\s*(?P<traces>.+)$",
|
|
84
|
+
re.IGNORECASE,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# FR-N / NFR-N definitions. Matches lines like:
|
|
88
|
+
#
|
|
89
|
+
# FR-1: Description
|
|
90
|
+
# - **FR-1**: Description
|
|
91
|
+
# * NFR-2 -- Description
|
|
92
|
+
#
|
|
93
|
+
_REQ_DEF_RE = re.compile(
|
|
94
|
+
r"^\s*(?:[-*]\s+)?"
|
|
95
|
+
r"\*{0,2}\s*(?P<id>(?:FR|NFR)-\d+)\s*\*{0,2}"
|
|
96
|
+
r"\s*[:\-]+\s*"
|
|
97
|
+
r"(?P<desc>.+?)\s*$",
|
|
98
|
+
re.IGNORECASE,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Trace-ID extractor used by 495-3. Accepts "FR-1", "NFR-12", comma or
|
|
102
|
+
# space separated lists; returns upper-cased IDs in source order.
|
|
103
|
+
_TRACE_ID_RE = re.compile(r"(?:FR|NFR)-\d+", re.IGNORECASE)
|
|
104
|
+
|
|
105
|
+
# Status mapping from pre-v0.20 SPEC.md tags to vBRIEF status values.
|
|
106
|
+
_SPEC_STATUS_TO_VBRIEF: dict[str, str] = {
|
|
107
|
+
"done": "completed",
|
|
108
|
+
"completed": "completed",
|
|
109
|
+
"complete": "completed",
|
|
110
|
+
"pending": "pending",
|
|
111
|
+
"running": "running",
|
|
112
|
+
"in-progress": "running",
|
|
113
|
+
"in_progress": "running",
|
|
114
|
+
"blocked": "blocked",
|
|
115
|
+
"cancelled": "cancelled",
|
|
116
|
+
"canceled": "cancelled",
|
|
117
|
+
"draft": "draft",
|
|
118
|
+
"proposed": "proposed",
|
|
119
|
+
"approved": "approved",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def map_spec_status(raw: str | None) -> str:
|
|
124
|
+
"""Map a SPECIFICATION.md status token to a vBRIEF status (D2 vocabulary).
|
|
125
|
+
|
|
126
|
+
Unknown or empty tokens default to ``pending``. ``[done]`` (historic
|
|
127
|
+
pre-v0.20 marker) -> ``completed`` (#499 handles the folder routing).
|
|
128
|
+
"""
|
|
129
|
+
if not raw:
|
|
130
|
+
return "pending"
|
|
131
|
+
return _SPEC_STATUS_TO_VBRIEF.get(raw.strip().lower(), "pending")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def parse_spec_tasks(content: str) -> list[dict]:
|
|
135
|
+
"""Parse ``tX.Y.Z`` task sections out of SPECIFICATION.md.
|
|
136
|
+
|
|
137
|
+
Returns a list of dicts with keys:
|
|
138
|
+
- ``task_id`` (str, e.g. ``"t1.1.1"``)
|
|
139
|
+
- ``title`` (str)
|
|
140
|
+
- ``status`` (str, vBRIEF vocabulary)
|
|
141
|
+
- ``body`` (str, multi-paragraph description)
|
|
142
|
+
- ``depends_on`` (list[str], task IDs this task depends on)
|
|
143
|
+
- ``traces`` (list[str], FR/NFR IDs -- verbatim, uppercased)
|
|
144
|
+
- ``acceptance`` (list[str], acceptance-criteria bullets)
|
|
145
|
+
- ``start_line`` (int, 1-indexed)
|
|
146
|
+
- ``end_line`` (int, 1-indexed)
|
|
147
|
+
"""
|
|
148
|
+
if not content:
|
|
149
|
+
return []
|
|
150
|
+
lines = content.splitlines()
|
|
151
|
+
tasks: list[dict] = []
|
|
152
|
+
current: dict | None = None
|
|
153
|
+
current_start = 0
|
|
154
|
+
current_body_lines: list[str] = []
|
|
155
|
+
|
|
156
|
+
def _flush(end_line: int) -> None:
|
|
157
|
+
if current is None:
|
|
158
|
+
return
|
|
159
|
+
body_lines = list(current_body_lines)
|
|
160
|
+
# Split body into: description paragraphs, Depends-on, Traces, and
|
|
161
|
+
# acceptance-criteria bullet list. Everything non-classified
|
|
162
|
+
# becomes part of the description.
|
|
163
|
+
depends: list[str] = []
|
|
164
|
+
traces: list[str] = []
|
|
165
|
+
acceptance: list[str] = []
|
|
166
|
+
description_lines: list[str] = []
|
|
167
|
+
in_acceptance = False
|
|
168
|
+
for raw in body_lines:
|
|
169
|
+
stripped = raw.strip()
|
|
170
|
+
dep_match = _DEPENDS_ON_RE.match(stripped)
|
|
171
|
+
if dep_match:
|
|
172
|
+
deps_raw = dep_match.group("deps").strip()
|
|
173
|
+
if deps_raw.lower() not in ("none", "n/a", "-"):
|
|
174
|
+
for tok in re.split(r"[,\s]+", deps_raw):
|
|
175
|
+
tok = tok.strip("`*,;. ")
|
|
176
|
+
if tok:
|
|
177
|
+
depends.append(tok)
|
|
178
|
+
in_acceptance = False
|
|
179
|
+
continue
|
|
180
|
+
trace_match = _TRACES_RE.match(stripped)
|
|
181
|
+
if trace_match:
|
|
182
|
+
for m in _TRACE_ID_RE.finditer(trace_match.group("traces")):
|
|
183
|
+
traces.append(m.group(0).upper())
|
|
184
|
+
in_acceptance = False
|
|
185
|
+
continue
|
|
186
|
+
if re.match(r"^\*{0,2}\s*Acceptance(?:\s+criteria)?\*{0,2}\s*:?\s*$",
|
|
187
|
+
stripped, re.IGNORECASE):
|
|
188
|
+
in_acceptance = True
|
|
189
|
+
continue
|
|
190
|
+
# Blank lines preserve acceptance-capture state -- a blank
|
|
191
|
+
# line between ``Acceptance criteria:`` and its first bullet
|
|
192
|
+
# MUST NOT reset ``in_acceptance`` (PR #525 Greptile P1).
|
|
193
|
+
if not stripped:
|
|
194
|
+
if not in_acceptance:
|
|
195
|
+
description_lines.append(raw)
|
|
196
|
+
continue
|
|
197
|
+
if stripped.startswith(("-", "*")) and in_acceptance:
|
|
198
|
+
acceptance.append(re.sub(r"^[-*]\s+", "", stripped))
|
|
199
|
+
continue
|
|
200
|
+
if (
|
|
201
|
+
stripped.startswith(("-", "*"))
|
|
202
|
+
and not in_acceptance
|
|
203
|
+
and not description_lines
|
|
204
|
+
):
|
|
205
|
+
# Loose bullet list at the START of the task body (before any
|
|
206
|
+
# description prose) counts as acceptance criteria when it
|
|
207
|
+
# looks like one (each bullet is a testable assertion).
|
|
208
|
+
# Conservative: only capture when no description prose has
|
|
209
|
+
# been accumulated yet -- bullets that appear after prose
|
|
210
|
+
# are description-area bullets (design notes, prerequisites)
|
|
211
|
+
# and must stay in description to preserve Agent B's
|
|
212
|
+
# reconciliation "SPEC owns body" routing (Greptile #525 P1).
|
|
213
|
+
acceptance.append(re.sub(r"^[-*]\s+", "", stripped))
|
|
214
|
+
continue
|
|
215
|
+
if stripped.startswith(("-", "*")) and not in_acceptance:
|
|
216
|
+
# Bullet after description prose -> treat as description.
|
|
217
|
+
description_lines.append(raw)
|
|
218
|
+
continue
|
|
219
|
+
if not stripped:
|
|
220
|
+
# Blank line: preserve in_acceptance across it so patterns
|
|
221
|
+
# like ``Acceptance criteria:\n\n- first bullet`` still
|
|
222
|
+
# capture into the acceptance list (Greptile #525 P1).
|
|
223
|
+
description_lines.append(raw)
|
|
224
|
+
continue
|
|
225
|
+
description_lines.append(raw)
|
|
226
|
+
in_acceptance = False
|
|
227
|
+
|
|
228
|
+
body = "\n".join(description_lines).strip()
|
|
229
|
+
current["body"] = body
|
|
230
|
+
current["depends_on"] = depends
|
|
231
|
+
current["traces"] = traces
|
|
232
|
+
current["acceptance"] = acceptance
|
|
233
|
+
current["start_line"] = current_start
|
|
234
|
+
current["end_line"] = end_line
|
|
235
|
+
tasks.append(current)
|
|
236
|
+
|
|
237
|
+
for idx, line in enumerate(lines, start=1):
|
|
238
|
+
heading = _TASK_HEADING_RE.match(line)
|
|
239
|
+
if heading:
|
|
240
|
+
_flush(idx - 1)
|
|
241
|
+
current = {
|
|
242
|
+
"task_id": heading.group("task_id").strip(),
|
|
243
|
+
"title": heading.group("title").strip(),
|
|
244
|
+
"status": map_spec_status(heading.group("status")),
|
|
245
|
+
}
|
|
246
|
+
current_start = idx
|
|
247
|
+
current_body_lines = []
|
|
248
|
+
continue
|
|
249
|
+
# New non-task ## heading closes the current task.
|
|
250
|
+
if re.match(r"^##\s+", line) and current is not None:
|
|
251
|
+
_flush(idx - 1)
|
|
252
|
+
current = None
|
|
253
|
+
current_body_lines = []
|
|
254
|
+
continue
|
|
255
|
+
if current is not None:
|
|
256
|
+
current_body_lines.append(line)
|
|
257
|
+
|
|
258
|
+
_flush(len(lines))
|
|
259
|
+
return tasks
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# FR / NFR definition parsing + Requirements narrative (495-4)
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_requirement_definitions(content: str) -> dict[str, str]:
|
|
268
|
+
"""Parse FR-N / NFR-N definitions from SPECIFICATION.md.
|
|
269
|
+
|
|
270
|
+
Looks inside any ``## Requirements`` / ``## Functional Requirements``
|
|
271
|
+
/ ``## Non-Functional Requirements`` sections (resolved via
|
|
272
|
+
_vbrief_legacy's known-mappings) and returns a dict of
|
|
273
|
+
``{"FR-1": "description", "NFR-2": "description"}`` preserving source
|
|
274
|
+
order via dict insertion order.
|
|
275
|
+
|
|
276
|
+
Only the FIRST definition wins for any given ID so that renumbered or
|
|
277
|
+
re-quoted IDs in later sections do not silently overwrite the
|
|
278
|
+
canonical definition.
|
|
279
|
+
"""
|
|
280
|
+
if not content:
|
|
281
|
+
return {}
|
|
282
|
+
sections = parse_top_level_sections(content)
|
|
283
|
+
requirements: dict[str, str] = {}
|
|
284
|
+
for title, body, _start, _end in sections:
|
|
285
|
+
canonical = lookup_canonical(title, SPEC_KNOWN_MAPPINGS)
|
|
286
|
+
if canonical not in ("Requirements", "NonFunctionalRequirements"):
|
|
287
|
+
continue
|
|
288
|
+
for line in body.splitlines():
|
|
289
|
+
match = _REQ_DEF_RE.match(line)
|
|
290
|
+
if not match:
|
|
291
|
+
continue
|
|
292
|
+
req_id = match.group("id").upper()
|
|
293
|
+
desc = match.group("desc").strip()
|
|
294
|
+
# Trim any trailing markdown emphasis / period noise
|
|
295
|
+
desc = re.sub(r"\s*\*+\s*$", "", desc).strip()
|
|
296
|
+
if req_id and desc and req_id not in requirements:
|
|
297
|
+
requirements[req_id] = desc
|
|
298
|
+
return requirements
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def build_requirements_narrative(requirements: dict[str, str]) -> str:
|
|
302
|
+
"""Render FR/NFR definitions as a Requirements narrative string.
|
|
303
|
+
|
|
304
|
+
Output is deterministic: FR-N first (numeric order), then NFR-N
|
|
305
|
+
(numeric order). Each line is ``{ID}: {Description}``.
|
|
306
|
+
"""
|
|
307
|
+
if not requirements:
|
|
308
|
+
return ""
|
|
309
|
+
|
|
310
|
+
def _sort_key(item: tuple[str, str]) -> tuple[int, int]:
|
|
311
|
+
rid, _ = item
|
|
312
|
+
kind = 0 if rid.startswith("FR-") else 1
|
|
313
|
+
num = int(rid.split("-", 1)[1]) if "-" in rid else 0
|
|
314
|
+
return (kind, num)
|
|
315
|
+
|
|
316
|
+
sorted_items = sorted(requirements.items(), key=_sort_key)
|
|
317
|
+
return "\n".join(f"{rid}: {desc}" for rid, desc in sorted_items)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# plan.edges[] extraction (495-6)
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_edges_from_tasks(tasks: Iterable[dict]) -> list[dict]:
|
|
326
|
+
"""Build ``plan.edges[]`` from per-task ``depends_on`` lists.
|
|
327
|
+
|
|
328
|
+
Each ``Depends on:`` item yields an edge ``{from, to, type}`` where
|
|
329
|
+
``from`` is the dependency ID and ``to`` is the current task ID, edge
|
|
330
|
+
type is ``"blocks"``. Self-edges and duplicates are suppressed. The
|
|
331
|
+
returned edges conform to the vBRIEF schema ID pattern
|
|
332
|
+
``^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$``; edges whose source or target
|
|
333
|
+
would violate the pattern are silently dropped.
|
|
334
|
+
"""
|
|
335
|
+
edges: list[dict] = []
|
|
336
|
+
seen: set[tuple[str, str]] = set()
|
|
337
|
+
id_pattern = re.compile(r"^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$")
|
|
338
|
+
for task in tasks:
|
|
339
|
+
tgt = str(task.get("task_id", "")).strip()
|
|
340
|
+
if not tgt or not id_pattern.match(tgt):
|
|
341
|
+
continue
|
|
342
|
+
for dep in task.get("depends_on", []) or []:
|
|
343
|
+
src = str(dep or "").strip().strip("`")
|
|
344
|
+
if not src or src == tgt or not id_pattern.match(src):
|
|
345
|
+
continue
|
|
346
|
+
key = (src, tgt)
|
|
347
|
+
if key in seen:
|
|
348
|
+
continue
|
|
349
|
+
seen.add(key)
|
|
350
|
+
edges.append({"from": src, "to": tgt, "type": "blocks"})
|
|
351
|
+
return edges
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# Narrative-key alignment per #506 D3 (495-9)
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def align_spec_narratives(narratives: dict[str, str]) -> dict[str, str]:
|
|
360
|
+
"""Reduce a narratives dict to the #506 D3 canonical spec shape.
|
|
361
|
+
|
|
362
|
+
- PascalCase keys win. Old spellings (``tech stack``, ``problem``,
|
|
363
|
+
etc.) are rewritten through the known-mappings list.
|
|
364
|
+
- Legacy keys that DO have a canonical mapping are folded under the
|
|
365
|
+
canonical key with body preservation (non-destructive merge).
|
|
366
|
+
- Anything not canonical is left in place so the caller can surface
|
|
367
|
+
it to ``LegacyArtifacts`` via _vbrief_legacy.
|
|
368
|
+
|
|
369
|
+
Returns a new dict; does not mutate the input.
|
|
370
|
+
"""
|
|
371
|
+
if not isinstance(narratives, dict):
|
|
372
|
+
return {}
|
|
373
|
+
result: dict[str, str] = {}
|
|
374
|
+
for key, value in narratives.items():
|
|
375
|
+
if not isinstance(value, str):
|
|
376
|
+
continue
|
|
377
|
+
canonical = lookup_canonical(key, SPEC_KNOWN_MAPPINGS)
|
|
378
|
+
target = canonical or key
|
|
379
|
+
if target in result:
|
|
380
|
+
result[target] = result[target].rstrip() + "\n\n" + value.strip()
|
|
381
|
+
else:
|
|
382
|
+
result[target] = value.strip()
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
# SPEC.md -> spec.vbrief.json narrative routing + migration log (495-15)
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def ingest_spec_narratives(
|
|
392
|
+
spec_content: str,
|
|
393
|
+
source_file: str = "SPECIFICATION.md",
|
|
394
|
+
) -> tuple[dict[str, str], list[dict], list[tuple[str, str, int, int]]]:
|
|
395
|
+
"""Split SPECIFICATION.md into canonical narratives + legacy sections.
|
|
396
|
+
|
|
397
|
+
Returns ``(canonical_narratives, log_entries, legacy_sections)`` where
|
|
398
|
+
``log_entries`` is a list of dicts suitable for the disambiguated
|
|
399
|
+
migration log (``{source, line_range, target_key, target_file}``).
|
|
400
|
+
"""
|
|
401
|
+
sections = parse_top_level_sections(spec_content or "")
|
|
402
|
+
canonical, legacy = partition_sections(sections, SPEC_KNOWN_MAPPINGS)
|
|
403
|
+
|
|
404
|
+
# Build disambiguated log entries per 495-15.
|
|
405
|
+
log_entries: list[dict] = []
|
|
406
|
+
for title, _body, start, end in sections:
|
|
407
|
+
canonical_key = lookup_canonical(title, SPEC_KNOWN_MAPPINGS)
|
|
408
|
+
target_file = "specification.vbrief.json"
|
|
409
|
+
target_key = (
|
|
410
|
+
canonical_key if canonical_key is not None else "LegacyArtifacts"
|
|
411
|
+
)
|
|
412
|
+
log_entries.append({
|
|
413
|
+
"source": source_file,
|
|
414
|
+
"section_title": title,
|
|
415
|
+
"line_range": f"{start}-{end}" if end > start else f"{start}",
|
|
416
|
+
"target_key": target_key,
|
|
417
|
+
"target_file": target_file,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
return canonical, log_entries, legacy
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def format_migration_log_entry(entry: dict) -> str:
|
|
424
|
+
"""Format a routing-decision dict as a single migrator log line.
|
|
425
|
+
|
|
426
|
+
Example output::
|
|
427
|
+
|
|
428
|
+
ROUTE SPECIFICATION.md:12-34 -> Overview -> specification.vbrief.json
|
|
429
|
+
"""
|
|
430
|
+
src = entry.get("source", "?")
|
|
431
|
+
rng = entry.get("line_range", "?")
|
|
432
|
+
key = entry.get("target_key", "?")
|
|
433
|
+
dst = entry.get("target_file", "?")
|
|
434
|
+
return f"ROUTE {src}:{rng} -> {key} -> {dst}"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# Per-task scope-vBRIEF narratives (495-1)
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def task_scope_narratives(task: dict) -> dict[str, str]:
|
|
443
|
+
"""Build the per-task scope-vBRIEF narrative dict.
|
|
444
|
+
|
|
445
|
+
Emits ``Description`` / ``DependsOn`` / ``AcceptanceCriteria`` /
|
|
446
|
+
``Traces`` narratives populated from :func:`parse_spec_tasks` output.
|
|
447
|
+
Empty values are omitted so the scope vBRIEF stays clean on tasks
|
|
448
|
+
that carried only a title.
|
|
449
|
+
"""
|
|
450
|
+
narratives: dict[str, str] = {}
|
|
451
|
+
body = (task.get("body") or "").strip()
|
|
452
|
+
if body:
|
|
453
|
+
narratives["Description"] = body
|
|
454
|
+
depends = task.get("depends_on") or []
|
|
455
|
+
if depends:
|
|
456
|
+
narratives["DependsOn"] = ", ".join(depends)
|
|
457
|
+
acceptance = task.get("acceptance") or []
|
|
458
|
+
if acceptance:
|
|
459
|
+
narratives["AcceptanceCriteria"] = "\n".join(
|
|
460
|
+
f"- {item}" for item in acceptance
|
|
461
|
+
)
|
|
462
|
+
traces = task.get("traces") or []
|
|
463
|
+
if traces:
|
|
464
|
+
narratives["Traces"] = ", ".join(traces)
|
|
465
|
+
return narratives
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
__all__ = [
|
|
469
|
+
"CANONICAL_SPEC_KEYS",
|
|
470
|
+
"align_spec_narratives",
|
|
471
|
+
"build_edges_from_tasks",
|
|
472
|
+
"build_requirements_narrative",
|
|
473
|
+
"format_migration_log_entry",
|
|
474
|
+
"ingest_spec_narratives",
|
|
475
|
+
"map_spec_status",
|
|
476
|
+
"parse_requirement_definitions",
|
|
477
|
+
"parse_spec_tasks",
|
|
478
|
+
"task_scope_narratives",
|
|
479
|
+
]
|