@deftai/directive-content 0.59.0 → 0.61.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 +10 -128
- package/.githooks/pre-push +8 -108
- package/Taskfile.yml +48 -58
- package/UPGRADING.md +19 -3
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/skills/skills-pack-0.1.json +1 -1
- package/packs/strategies/strategies-pack-0.1.json +19 -19
- package/scm/github.md +37 -6
- package/skills/deft-directive-setup/SKILL.md +24 -15
- package/strategies/speckit.md +14 -14
- package/strategies/v0-20-contract.md +12 -1
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +26 -0
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -1
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2552
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
|
@@ -1,589 +0,0 @@
|
|
|
1
|
-
"""_vbrief_legacy.py -- LegacyArtifacts mechanism for migrate:vbrief (#505).
|
|
2
|
-
|
|
3
|
-
Shared known-mappings list + normalization for both canonical extraction
|
|
4
|
-
(#495, consumed by ``_vbrief_fidelity``) and non-canonical capture (#505,
|
|
5
|
-
consumed by ``migrate_vbrief``). Hard-coded for v0.20 per #506 D5;
|
|
6
|
-
config-driven extensibility is a v0.21+ feature request.
|
|
7
|
-
|
|
8
|
-
Exports
|
|
9
|
-
-------
|
|
10
|
-
SPEC_KNOWN_MAPPINGS, PROJECT_KNOWN_MAPPINGS
|
|
11
|
-
Normalized-heading -> canonical-narrative-key dicts covering the locked
|
|
12
|
-
v0.20 aliases per #506 D5.
|
|
13
|
-
normalize_title(title)
|
|
14
|
-
Four-rule normalization: case-insensitive + whitespace-collapsed +
|
|
15
|
-
punctuation-stripped + word-separator-tolerant.
|
|
16
|
-
lookup_canonical(title, mapping)
|
|
17
|
-
Return the canonical key for a heading, or None if unknown (legacy).
|
|
18
|
-
parse_top_level_sections(content)
|
|
19
|
-
Split markdown content at top-level ``## `` boundaries; returns a list
|
|
20
|
-
of ``(title, body, start_line, end_line)`` tuples. Substructure (H3
|
|
21
|
-
etc.) is preserved verbatim inside each body.
|
|
22
|
-
partition_sections(sections, mapping)
|
|
23
|
-
Split parsed sections into canonical (matches known-mappings) and
|
|
24
|
-
legacy (no match) buckets.
|
|
25
|
-
emit_legacy_artifacts(legacy_sections, source_file, project_root, *, slugify_fn,
|
|
26
|
-
warning_prefix=None, event_emitter=None)
|
|
27
|
-
Build the LegacyArtifacts narrative string for one vBRIEF file, write
|
|
28
|
-
any >6 KB sidecars under ``vbrief/legacy/``, and return
|
|
29
|
-
``(narrative_str, sidecar_paths, stats)``. When ``event_emitter`` is
|
|
30
|
-
supplied, also emits one ``legacy:detected`` framework event per
|
|
31
|
-
captured section via the callback (#635 events behavioral wiring).
|
|
32
|
-
emit_legacy_report(project_root, captures)
|
|
33
|
-
Write ``vbrief/migration/LEGACY-REPORT.md`` per #505 Section 6.
|
|
34
|
-
detect_prd_legacy(prd_content, canonical_specification_keys, *, source_name)
|
|
35
|
-
PRD.md section-name diff (OQ3-b): sections whose normalized title does
|
|
36
|
-
NOT match a canonical spec narrative key are captured with the
|
|
37
|
-
hand-edit warning prefix.
|
|
38
|
-
|
|
39
|
-
Issue: #505, #506 D5. Shared with #495 via ``_vbrief_fidelity``.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
from __future__ import annotations
|
|
43
|
-
|
|
44
|
-
import contextlib
|
|
45
|
-
import re
|
|
46
|
-
from collections.abc import Callable
|
|
47
|
-
from datetime import UTC, datetime
|
|
48
|
-
from pathlib import Path
|
|
49
|
-
|
|
50
|
-
# ---------------------------------------------------------------------------
|
|
51
|
-
# Constants
|
|
52
|
-
# ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
# 6 KB inline threshold per #506 D5. Sections whose preserved text exceeds
|
|
55
|
-
# this limit overflow to ``vbrief/legacy/{stem}-{slug}.md``.
|
|
56
|
-
INLINE_THRESHOLD_BYTES: int = 6 * 1024
|
|
57
|
-
|
|
58
|
-
# Hand-edit warning prefix for PRD.md captured content (#505 Section 5).
|
|
59
|
-
PRD_HAND_EDIT_WARNING: str = (
|
|
60
|
-
"> WARNING: PRD.md was edited manually in this project. PRD.md is "
|
|
61
|
-
"framework-defined\n"
|
|
62
|
-
"> as a rendered export from specification.vbrief.json. Manual edits "
|
|
63
|
-
"here are\n"
|
|
64
|
-
"> against framework guidance; review whether this content should be "
|
|
65
|
-
"migrated\n"
|
|
66
|
-
"> into a specification.vbrief.json narrative."
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# ---------------------------------------------------------------------------
|
|
71
|
-
# Normalization (four rules per #506 D5)
|
|
72
|
-
# ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def normalize_title(title: str) -> str:
|
|
76
|
-
"""Apply the four normalization rules from #506 D5 (+ CamelCase split).
|
|
77
|
-
|
|
78
|
-
1. Case-insensitive (lowercase)
|
|
79
|
-
2. Punctuation stripped (keep alphanumerics, spaces, hyphens, underscores)
|
|
80
|
-
3. Word-separator tolerant (``-`` / ``_`` / CamelCase / space equivalent)
|
|
81
|
-
4. Whitespace collapsed (runs of spaces -> single space; trim)
|
|
82
|
-
|
|
83
|
-
CamelCase splitting is treated as a word-separator equivalence so
|
|
84
|
-
``ProblemStatement`` and ``Problem Statement`` both normalize to
|
|
85
|
-
``problem statement`` (see #495/#506 D5 comment thread: word-separator
|
|
86
|
-
tolerance covers the prd_render.py no-space output).
|
|
87
|
-
"""
|
|
88
|
-
raw = title or ""
|
|
89
|
-
# Split CamelCase word boundaries BEFORE lowercasing so we can use
|
|
90
|
-
# ``[A-Z]`` detection: ``ProblemStatement`` -> ``Problem Statement``.
|
|
91
|
-
split = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", raw)
|
|
92
|
-
low = split.lower().strip()
|
|
93
|
-
low = re.sub(r"[^a-z0-9\s_\-]", " ", low)
|
|
94
|
-
low = re.sub(r"[-_]+", " ", low)
|
|
95
|
-
return re.sub(r"\s+", " ", low).strip()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# ---------------------------------------------------------------------------
|
|
99
|
-
# Known mappings (hard-coded v0.20 per #506 D5)
|
|
100
|
-
# ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
# Canonical narrative keys per #506 D3.
|
|
103
|
-
CANONICAL_SPEC_KEYS: tuple[str, ...] = (
|
|
104
|
-
"Overview",
|
|
105
|
-
"Architecture",
|
|
106
|
-
"ProblemStatement",
|
|
107
|
-
"Goals",
|
|
108
|
-
"UserStories",
|
|
109
|
-
"Requirements",
|
|
110
|
-
"NonFunctionalRequirements",
|
|
111
|
-
"SuccessMetrics",
|
|
112
|
-
"TestingStrategy",
|
|
113
|
-
"Deployment",
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
CANONICAL_PROJECT_KEYS: tuple[str, ...] = (
|
|
117
|
-
"TechStack",
|
|
118
|
-
"Strategy",
|
|
119
|
-
"Quality",
|
|
120
|
-
"ProjectRules",
|
|
121
|
-
"Branching",
|
|
122
|
-
"DeftVersion",
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# specification.vbrief.json aliases. Keys are normalized per
|
|
126
|
-
# normalize_title(); values are canonical PascalCase narrative keys.
|
|
127
|
-
SPEC_KNOWN_MAPPINGS: dict[str, str] = {
|
|
128
|
-
"overview": "Overview",
|
|
129
|
-
"summary": "Overview",
|
|
130
|
-
"architecture": "Architecture",
|
|
131
|
-
"system design": "Architecture",
|
|
132
|
-
"technical architecture": "Architecture",
|
|
133
|
-
"problem statement": "ProblemStatement",
|
|
134
|
-
"problem": "ProblemStatement",
|
|
135
|
-
"background": "ProblemStatement",
|
|
136
|
-
"goals": "Goals",
|
|
137
|
-
"objectives": "Goals",
|
|
138
|
-
"user stories": "UserStories",
|
|
139
|
-
"use cases": "UserStories",
|
|
140
|
-
"requirements": "Requirements",
|
|
141
|
-
"functional requirements": "Requirements",
|
|
142
|
-
"non functional requirements": "NonFunctionalRequirements",
|
|
143
|
-
"nfrs": "NonFunctionalRequirements",
|
|
144
|
-
"success metrics": "SuccessMetrics",
|
|
145
|
-
"acceptance criteria": "SuccessMetrics",
|
|
146
|
-
"acceptance criteria project level": "SuccessMetrics",
|
|
147
|
-
"testing strategy": "TestingStrategy",
|
|
148
|
-
"test plan": "TestingStrategy",
|
|
149
|
-
"testing": "TestingStrategy",
|
|
150
|
-
"deployment": "Deployment",
|
|
151
|
-
"deployment plan": "Deployment",
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# PROJECT-DEFINITION.vbrief.json aliases.
|
|
155
|
-
PROJECT_KNOWN_MAPPINGS: dict[str, str] = {
|
|
156
|
-
"tech stack": "TechStack",
|
|
157
|
-
"technology stack": "TechStack",
|
|
158
|
-
"stack": "TechStack",
|
|
159
|
-
"project configuration": "TechStack",
|
|
160
|
-
"strategy": "Strategy",
|
|
161
|
-
"quality": "Quality",
|
|
162
|
-
"standards": "Quality",
|
|
163
|
-
"quality standards": "Quality",
|
|
164
|
-
"project specific rules": "ProjectRules",
|
|
165
|
-
"project rules": "ProjectRules",
|
|
166
|
-
"custom rules": "ProjectRules",
|
|
167
|
-
"branching": "Branching",
|
|
168
|
-
"branching strategy": "Branching",
|
|
169
|
-
"git workflow": "Branching",
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def lookup_canonical(title: str, mapping: dict[str, str]) -> str | None:
|
|
174
|
-
"""Return the canonical key for ``title`` or None if not a known alias."""
|
|
175
|
-
return mapping.get(normalize_title(title))
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# ---------------------------------------------------------------------------
|
|
179
|
-
# Section parsing (top-level ## only per #506 D5)
|
|
180
|
-
# ---------------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def parse_top_level_sections(
|
|
184
|
-
content: str,
|
|
185
|
-
) -> list[tuple[str, str, int, int]]:
|
|
186
|
-
"""Split markdown at top-level ``## `` boundaries.
|
|
187
|
-
|
|
188
|
-
Returns a list of ``(title, body, start_line, end_line)`` tuples where
|
|
189
|
-
lines are 1-indexed. Substructure (``###`` and below) is preserved
|
|
190
|
-
verbatim inside each body -- the migrator MUST NOT attempt to re-parse
|
|
191
|
-
it (per #506 D5 / #505 Section 2).
|
|
192
|
-
|
|
193
|
-
Fenced code blocks are respected so that ``## ``-prefixed lines inside
|
|
194
|
-
a fence are not misread as section boundaries.
|
|
195
|
-
"""
|
|
196
|
-
if not content:
|
|
197
|
-
return []
|
|
198
|
-
|
|
199
|
-
lines = content.splitlines()
|
|
200
|
-
sections: list[tuple[str, str, int, int]] = []
|
|
201
|
-
in_fence = False
|
|
202
|
-
current_title: str | None = None
|
|
203
|
-
current_start = 0
|
|
204
|
-
current_body: list[str] = []
|
|
205
|
-
|
|
206
|
-
def _flush(end_line: int) -> None:
|
|
207
|
-
if current_title is None:
|
|
208
|
-
return
|
|
209
|
-
body = "\n".join(current_body).rstrip()
|
|
210
|
-
sections.append((current_title, body, current_start, end_line))
|
|
211
|
-
|
|
212
|
-
for idx, line in enumerate(lines, start=1):
|
|
213
|
-
stripped = line.lstrip()
|
|
214
|
-
# Track fences so we don't misinterpret ## inside code blocks.
|
|
215
|
-
if stripped.startswith("```"):
|
|
216
|
-
in_fence = not in_fence
|
|
217
|
-
if current_title is not None:
|
|
218
|
-
current_body.append(line)
|
|
219
|
-
continue
|
|
220
|
-
|
|
221
|
-
if not in_fence:
|
|
222
|
-
match = re.match(r"^##\s+(.+?)\s*$", line)
|
|
223
|
-
if match:
|
|
224
|
-
# Close previous section.
|
|
225
|
-
_flush(idx - 1)
|
|
226
|
-
current_title = match.group(1).strip()
|
|
227
|
-
current_start = idx
|
|
228
|
-
current_body = []
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
if current_title is not None:
|
|
232
|
-
current_body.append(line)
|
|
233
|
-
|
|
234
|
-
# Flush trailing section.
|
|
235
|
-
_flush(len(lines))
|
|
236
|
-
return sections
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def partition_sections(
|
|
240
|
-
sections: list[tuple[str, str, int, int]],
|
|
241
|
-
mapping: dict[str, str],
|
|
242
|
-
) -> tuple[dict[str, str], list[tuple[str, str, int, int]]]:
|
|
243
|
-
"""Split parsed sections into canonical vs legacy buckets.
|
|
244
|
-
|
|
245
|
-
Returns ``(canonical, legacy)`` where ``canonical`` is a dict of
|
|
246
|
-
``canonical_key -> body`` and ``legacy`` is the list of unmatched
|
|
247
|
-
``(title, body, start, end)`` tuples in source order.
|
|
248
|
-
|
|
249
|
-
When multiple aliases collapse onto the same canonical key, bodies are
|
|
250
|
-
joined with a blank line so no content is lost.
|
|
251
|
-
"""
|
|
252
|
-
canonical: dict[str, str] = {}
|
|
253
|
-
legacy: list[tuple[str, str, int, int]] = []
|
|
254
|
-
for title, body, start, end in sections:
|
|
255
|
-
key = lookup_canonical(title, mapping)
|
|
256
|
-
if key is None:
|
|
257
|
-
legacy.append((title, body, start, end))
|
|
258
|
-
continue
|
|
259
|
-
if not body.strip():
|
|
260
|
-
# Skip empty canonical sections (see existing
|
|
261
|
-
# _parse_prd_narratives behaviour).
|
|
262
|
-
continue
|
|
263
|
-
if key in canonical:
|
|
264
|
-
canonical[key] = canonical[key].rstrip() + "\n\n" + body.strip()
|
|
265
|
-
else:
|
|
266
|
-
canonical[key] = body.strip()
|
|
267
|
-
return canonical, legacy
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# ---------------------------------------------------------------------------
|
|
271
|
-
# LegacyArtifacts narrative construction + sidecar overflow
|
|
272
|
-
# ---------------------------------------------------------------------------
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _format_line_range(start: int, end: int) -> str:
|
|
276
|
-
"""Format a line-range for provenance headers."""
|
|
277
|
-
if end <= start:
|
|
278
|
-
return f"{start}"
|
|
279
|
-
return f"{start}-{end}"
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def emit_legacy_artifacts(
|
|
283
|
-
legacy_sections: list[tuple[str, str, int, int]],
|
|
284
|
-
source_file: str,
|
|
285
|
-
project_root: Path,
|
|
286
|
-
*,
|
|
287
|
-
slugify_fn: Callable[[str], str],
|
|
288
|
-
warning_prefix: str | None = None,
|
|
289
|
-
event_emitter: Callable[[str, dict], None] | None = None,
|
|
290
|
-
flagged: bool = False,
|
|
291
|
-
) -> tuple[str, list[Path], list[dict]]:
|
|
292
|
-
"""Build the LegacyArtifacts narrative for one vBRIEF file.
|
|
293
|
-
|
|
294
|
-
``source_file`` is the display name used in provenance headers (e.g.
|
|
295
|
-
``SPECIFICATION.md``). The basename-without-extension (lowercased) is
|
|
296
|
-
used as the sidecar ``{stem}`` (#506 D5 / #505 Section 4).
|
|
297
|
-
|
|
298
|
-
``slugify_fn`` converts section titles to lowercase-kebab-case
|
|
299
|
-
filenames; Agent D's slug-safe ID generator (#498) is preferred once
|
|
300
|
-
available, otherwise the repo's historic ``slugify`` works.
|
|
301
|
-
|
|
302
|
-
``warning_prefix`` optionally injects a warning block under each
|
|
303
|
-
section header -- used for PRD.md hand-edit captures (#505 Section 5).
|
|
304
|
-
|
|
305
|
-
``event_emitter`` is an optional ``(event_name, payload)`` callback
|
|
306
|
-
invoked once per captured section with ``event_name='legacy:detected'``
|
|
307
|
-
and the per-section stat dict as payload (#635 behavioral events
|
|
308
|
-
wiring). Defaulting to ``None`` keeps the existing API surface
|
|
309
|
-
bit-for-bit identical when callers do not opt in -- existing tests
|
|
310
|
-
and consumers continue to behave exactly as before.
|
|
311
|
-
|
|
312
|
-
``flagged`` (default ``False``) marks every captured section's stat
|
|
313
|
-
dict with ``"flagged": True`` BEFORE the event is emitted so the
|
|
314
|
-
``legacy:detected`` event payload accurately reflects the PRD.md
|
|
315
|
-
hand-edit provenance contract documented in ``events/registry.json``
|
|
316
|
-
under ``category: "behavioral"`` (Greptile #706 P1, post-#706
|
|
317
|
-
unification per #709 / #710). Callers that pass ``warning_prefix``
|
|
318
|
-
for PRD.md hand-edit captures SHOULD also pass ``flagged=True`` so
|
|
319
|
-
the structural emission matches the warning prefix in the
|
|
320
|
-
narrative.
|
|
321
|
-
|
|
322
|
-
Returns ``(narrative_str, sidecar_paths, stats)`` where ``stats`` is a
|
|
323
|
-
list of per-section dicts with keys: ``title``, ``source``, ``range``,
|
|
324
|
-
``size_bytes``, ``inline`` (bool), ``sidecar`` (str | None),
|
|
325
|
-
``flagged`` (bool, when ``flagged=True`` was passed),
|
|
326
|
-
``canonical_suggestion`` (str | None).
|
|
327
|
-
"""
|
|
328
|
-
if not legacy_sections:
|
|
329
|
-
return "", [], []
|
|
330
|
-
|
|
331
|
-
stem = Path(source_file).stem.lower()
|
|
332
|
-
legacy_dir = project_root / "vbrief" / "legacy"
|
|
333
|
-
narrative_parts: list[str] = []
|
|
334
|
-
sidecar_paths: list[Path] = []
|
|
335
|
-
stats: list[dict] = []
|
|
336
|
-
|
|
337
|
-
for title, body, start, end in legacy_sections:
|
|
338
|
-
header = (
|
|
339
|
-
f"### {title} (from {source_file}:"
|
|
340
|
-
f"{_format_line_range(start, end)})"
|
|
341
|
-
)
|
|
342
|
-
body_stripped = body.strip()
|
|
343
|
-
size = len(body_stripped.encode("utf-8"))
|
|
344
|
-
if size > INLINE_THRESHOLD_BYTES:
|
|
345
|
-
slug = slugify_fn(title) or slugify_fn(f"section-{start}")
|
|
346
|
-
sidecar_name = f"{stem}-{slug}.md"
|
|
347
|
-
sidecar = legacy_dir / sidecar_name
|
|
348
|
-
legacy_dir.mkdir(parents=True, exist_ok=True)
|
|
349
|
-
sidecar_content = (
|
|
350
|
-
f"# {title}\n\n"
|
|
351
|
-
f"> Captured from {source_file}:"
|
|
352
|
-
f"{_format_line_range(start, end)} during "
|
|
353
|
-
f"`task migrate:vbrief` (#505)\n\n"
|
|
354
|
-
f"{body_stripped}\n"
|
|
355
|
-
)
|
|
356
|
-
sidecar.write_text(sidecar_content, encoding="utf-8")
|
|
357
|
-
sidecar_paths.append(sidecar)
|
|
358
|
-
pointer = (
|
|
359
|
-
f"[Content exceeds inline threshold — "
|
|
360
|
-
f"see vbrief/legacy/{sidecar_name}]"
|
|
361
|
-
)
|
|
362
|
-
section_block = f"{header}\n{pointer}"
|
|
363
|
-
stats.append({
|
|
364
|
-
"title": title,
|
|
365
|
-
"source": source_file,
|
|
366
|
-
"range": _format_line_range(start, end),
|
|
367
|
-
"size_bytes": size,
|
|
368
|
-
"inline": False,
|
|
369
|
-
"sidecar": f"vbrief/legacy/{sidecar_name}",
|
|
370
|
-
})
|
|
371
|
-
else:
|
|
372
|
-
if warning_prefix:
|
|
373
|
-
section_block = (
|
|
374
|
-
f"{header}\n{warning_prefix}\n\n{body_stripped}"
|
|
375
|
-
)
|
|
376
|
-
else:
|
|
377
|
-
section_block = f"{header}\n\n{body_stripped}"
|
|
378
|
-
stats.append({
|
|
379
|
-
"title": title,
|
|
380
|
-
"source": source_file,
|
|
381
|
-
"range": _format_line_range(start, end),
|
|
382
|
-
"size_bytes": size,
|
|
383
|
-
"inline": True,
|
|
384
|
-
"sidecar": None,
|
|
385
|
-
})
|
|
386
|
-
# Apply the ``flagged`` annotation BEFORE emitting the event so
|
|
387
|
-
# the ``legacy:detected`` payload contract documented in
|
|
388
|
-
# ``events/registry.json`` (``category: "behavioral"``) is
|
|
389
|
-
# honoured for PRD.md hand-edit captures (Greptile #706 P1,
|
|
390
|
-
# post-#706 unification per #709 / #710). Previously the
|
|
391
|
-
# migrator patched this field on the returned stats AFTER the
|
|
392
|
-
# function had already emitted, leaving every PRD.md event
|
|
393
|
-
# missing the ``flagged`` field.
|
|
394
|
-
if flagged:
|
|
395
|
-
stats[-1]["flagged"] = True
|
|
396
|
-
narrative_parts.append(section_block)
|
|
397
|
-
if event_emitter is not None:
|
|
398
|
-
# Emit a structural ``legacy:detected`` framework event per
|
|
399
|
-
# captured section (#635 behavioral events wiring; the event
|
|
400
|
-
# contract lives in ``events/registry.json`` under
|
|
401
|
-
# ``category: "behavioral"`` post-#706 unification).
|
|
402
|
-
# Failures in the emitter MUST NOT break the migrator --
|
|
403
|
-
# legacy capture is the primary contract here, the event
|
|
404
|
-
# stream is an additive observability layer.
|
|
405
|
-
with contextlib.suppress(Exception):
|
|
406
|
-
event_emitter("legacy:detected", dict(stats[-1]))
|
|
407
|
-
|
|
408
|
-
narrative = "\n\n".join(narrative_parts).rstrip() + "\n"
|
|
409
|
-
return narrative, sidecar_paths, stats
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# ---------------------------------------------------------------------------
|
|
413
|
-
# LEGACY-REPORT.md emission (#505 Section 6)
|
|
414
|
-
# ---------------------------------------------------------------------------
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _render_size(size_bytes: int) -> str:
|
|
418
|
-
if size_bytes < 1024:
|
|
419
|
-
return f"{size_bytes} B"
|
|
420
|
-
return f"{size_bytes / 1024:.1f} KB"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def emit_legacy_report(
|
|
424
|
-
project_root: Path,
|
|
425
|
-
captures: dict[str, list[dict]],
|
|
426
|
-
*,
|
|
427
|
-
migrator_version: str,
|
|
428
|
-
sources: list[str],
|
|
429
|
-
timestamp: str | None = None,
|
|
430
|
-
) -> Path | None:
|
|
431
|
-
"""Write ``vbrief/migration/LEGACY-REPORT.md``.
|
|
432
|
-
|
|
433
|
-
``captures`` keys are report section labels (e.g.
|
|
434
|
-
``"specification.vbrief.json -> LegacyArtifacts"``) mapping to the
|
|
435
|
-
per-section stat dicts produced by :func:`emit_legacy_artifacts`.
|
|
436
|
-
|
|
437
|
-
``timestamp`` is an ISO-8601 ``YYYY-MM-DDTHH:MM:SSZ`` string; when
|
|
438
|
-
``None`` (default) the current UTC wall clock is used. Tests inject
|
|
439
|
-
a frozen value so the golden fixture can diff byte-for-byte without
|
|
440
|
-
a clock-freezing library (Greptile #525 P1).
|
|
441
|
-
|
|
442
|
-
Returns the path to the written file, or ``None`` if there is
|
|
443
|
-
nothing to report (all buckets empty).
|
|
444
|
-
"""
|
|
445
|
-
if not any(captures.values()):
|
|
446
|
-
return None
|
|
447
|
-
|
|
448
|
-
report_dir = project_root / "vbrief" / "migration"
|
|
449
|
-
report_dir.mkdir(parents=True, exist_ok=True)
|
|
450
|
-
report_path = report_dir / "LEGACY-REPORT.md"
|
|
451
|
-
|
|
452
|
-
now = timestamp or datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
453
|
-
lines: list[str] = [
|
|
454
|
-
"# Legacy content captured during migration",
|
|
455
|
-
"",
|
|
456
|
-
f"Generated: {now}",
|
|
457
|
-
f"Migrator version: {migrator_version}",
|
|
458
|
-
f"Sources: {', '.join(sources)}",
|
|
459
|
-
"",
|
|
460
|
-
]
|
|
461
|
-
|
|
462
|
-
for label, items in captures.items():
|
|
463
|
-
lines.append(f"## {label}")
|
|
464
|
-
lines.append("")
|
|
465
|
-
if not items:
|
|
466
|
-
lines.append("(none)")
|
|
467
|
-
lines.append("")
|
|
468
|
-
continue
|
|
469
|
-
for item in items:
|
|
470
|
-
rng = item.get("range", "?")
|
|
471
|
-
src = item.get("source", "?")
|
|
472
|
-
title = item.get("title", "Untitled")
|
|
473
|
-
inline = item.get("inline", True)
|
|
474
|
-
size = _render_size(int(item.get("size_bytes", 0)))
|
|
475
|
-
sidecar = item.get("sidecar")
|
|
476
|
-
flagged = bool(item.get("flagged"))
|
|
477
|
-
|
|
478
|
-
lines.append(f"### {title} ({src}:{rng})")
|
|
479
|
-
disposition = "inline" if inline else f"sidecar: {sidecar}"
|
|
480
|
-
lines.append(f"- Size: {size} ({disposition})")
|
|
481
|
-
reason = item.get("reason") or (
|
|
482
|
-
"No canonical narrative match; captured verbatim to preserve "
|
|
483
|
-
"intent."
|
|
484
|
-
)
|
|
485
|
-
lines.append(f"- Reason: {reason}")
|
|
486
|
-
lines.append(
|
|
487
|
-
"- Suggested disposition: review during "
|
|
488
|
-
"`deft-directive-sync` Phase 6c Legacy Artifact Review."
|
|
489
|
-
)
|
|
490
|
-
lines.append("- Action options:")
|
|
491
|
-
lines.append(" - Keep as LegacyArtifacts (no action)")
|
|
492
|
-
lines.append(" - Fold into an existing canonical narrative")
|
|
493
|
-
lines.append(" - Drop (confirm nothing important is lost)")
|
|
494
|
-
if flagged:
|
|
495
|
-
lines.append(
|
|
496
|
-
"- Flag: PRD.md was hand-edited -- content does not "
|
|
497
|
-
"match any canonical specification narrative name."
|
|
498
|
-
)
|
|
499
|
-
lines.append("")
|
|
500
|
-
|
|
501
|
-
report_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
502
|
-
return report_path
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
# ---------------------------------------------------------------------------
|
|
506
|
-
# PRD.md section-name diff (OQ3-b per #505 Section 5)
|
|
507
|
-
# ---------------------------------------------------------------------------
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def detect_prd_legacy(
|
|
511
|
-
prd_content: str,
|
|
512
|
-
canonical_keys_present: set[str],
|
|
513
|
-
*,
|
|
514
|
-
source_name: str = "PRD.md",
|
|
515
|
-
) -> list[tuple[str, str, int, int]]:
|
|
516
|
-
"""Return PRD.md sections that are not render output of canonical keys.
|
|
517
|
-
|
|
518
|
-
Per #506 D5 / #505 Section 5 (OQ3-b): section-title diff only. A
|
|
519
|
-
section whose normalized title maps to a canonical spec narrative key
|
|
520
|
-
that IS present in the post-migration spec vBRIEF is treated as
|
|
521
|
-
expected render output and NOT captured. Everything else is treated
|
|
522
|
-
as hand-edited content and returned for legacy capture.
|
|
523
|
-
|
|
524
|
-
``canonical_keys_present`` is the set of canonical narrative keys that
|
|
525
|
-
actually exist on the spec vBRIEF after migration (e.g.
|
|
526
|
-
``{"Overview", "Goals"}``) -- the caller computes this.
|
|
527
|
-
"""
|
|
528
|
-
sections = parse_top_level_sections(prd_content or "")
|
|
529
|
-
legacy: list[tuple[str, str, int, int]] = []
|
|
530
|
-
for title, body, start, end in sections:
|
|
531
|
-
canonical = lookup_canonical(title, SPEC_KNOWN_MAPPINGS)
|
|
532
|
-
if canonical and canonical in canonical_keys_present:
|
|
533
|
-
continue
|
|
534
|
-
legacy.append((title, body, start, end))
|
|
535
|
-
return legacy
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
# ---------------------------------------------------------------------------
|
|
539
|
-
# Stdout summary (#505 Section 8)
|
|
540
|
-
# ---------------------------------------------------------------------------
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def summarize_captures(captures: dict[str, list[dict]]) -> list[str]:
|
|
544
|
-
"""Return stdout-summary lines for the end-of-run migrator output."""
|
|
545
|
-
if not any(captures.values()):
|
|
546
|
-
return []
|
|
547
|
-
lines = ["", "LEGACY CONTENT CAPTURED:"]
|
|
548
|
-
total_sidecars = 0
|
|
549
|
-
for label, items in captures.items():
|
|
550
|
-
inline_size = sum(
|
|
551
|
-
int(i.get("size_bytes", 0)) for i in items if i.get("inline")
|
|
552
|
-
)
|
|
553
|
-
total_sidecars += sum(1 for i in items if not i.get("inline"))
|
|
554
|
-
flagged = " (flagged: hand-edited)" if any(
|
|
555
|
-
i.get("flagged") for i in items
|
|
556
|
-
) else ""
|
|
557
|
-
lines.append(
|
|
558
|
-
f" {label}: {len(items)} section(s) "
|
|
559
|
-
f"({_render_size(inline_size)} inline){flagged}"
|
|
560
|
-
)
|
|
561
|
-
lines.append(f" Sidecar files: {total_sidecars}")
|
|
562
|
-
lines.append("")
|
|
563
|
-
lines.append(
|
|
564
|
-
" Full list and suggested dispositions: "
|
|
565
|
-
"vbrief/migration/LEGACY-REPORT.md"
|
|
566
|
-
)
|
|
567
|
-
lines.append(
|
|
568
|
-
" Review with: `task sync` (or any session-start sync) -- "
|
|
569
|
-
"agent will walk you through each item."
|
|
570
|
-
)
|
|
571
|
-
return lines
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
__all__ = [
|
|
575
|
-
"CANONICAL_PROJECT_KEYS",
|
|
576
|
-
"CANONICAL_SPEC_KEYS",
|
|
577
|
-
"INLINE_THRESHOLD_BYTES",
|
|
578
|
-
"PRD_HAND_EDIT_WARNING",
|
|
579
|
-
"PROJECT_KNOWN_MAPPINGS",
|
|
580
|
-
"SPEC_KNOWN_MAPPINGS",
|
|
581
|
-
"detect_prd_legacy",
|
|
582
|
-
"emit_legacy_artifacts",
|
|
583
|
-
"emit_legacy_report",
|
|
584
|
-
"lookup_canonical",
|
|
585
|
-
"normalize_title",
|
|
586
|
-
"parse_top_level_sections",
|
|
587
|
-
"partition_sections",
|
|
588
|
-
"summarize_captures",
|
|
589
|
-
]
|