@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,336 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""pack_migrate_lessons.py -- one-shot migration: meta/lessons.md -> structured pack.
|
|
3
|
-
|
|
4
|
-
Parses the hand-authored ``meta/lessons.md`` into the canonical structured
|
|
5
|
-
source ``packs/lessons/lessons-pack-0.1.json`` (the source of truth per
|
|
6
|
-
ADR-001; ``meta/lessons.md`` then becomes a regenerated projection via
|
|
7
|
-
``scripts/pack_render.py``).
|
|
8
|
-
|
|
9
|
-
Parsing model
|
|
10
|
-
-------------
|
|
11
|
-
The document is split on top-level ``## `` headings. Everything before the
|
|
12
|
-
first ``## `` heading (the ``# Lessons Learned`` title + any authoring
|
|
13
|
-
comment) is document chrome owned by the renderer and is discarded here.
|
|
14
|
-
For each section:
|
|
15
|
-
|
|
16
|
-
- ``title`` is the full heading text (verbatim, so the projection round-trips).
|
|
17
|
-
- ``date`` is the ``YYYY-MM`` extracted from the heading parenthetical, or null.
|
|
18
|
-
- ``issue_refs`` is every ``#NNN`` reference in the heading, in order.
|
|
19
|
-
- ``source`` is the text of the body's ``**Source:**`` line, or null.
|
|
20
|
-
- ``tags`` is 1-3 tags from the controlled vocabulary, scored from keywords.
|
|
21
|
-
- ``body`` is the full section body verbatim (lossless blob).
|
|
22
|
-
|
|
23
|
-
Usage::
|
|
24
|
-
|
|
25
|
-
uv run python scripts/pack_migrate_lessons.py [--source meta/lessons.md] \\
|
|
26
|
-
[--out packs/lessons/lessons-pack-0.1.json]
|
|
27
|
-
|
|
28
|
-
Exit codes:
|
|
29
|
-
0 -- migrated successfully
|
|
30
|
-
1 -- source markdown missing or empty
|
|
31
|
-
2 -- usage error
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
from __future__ import annotations
|
|
35
|
-
|
|
36
|
-
import argparse
|
|
37
|
-
import json
|
|
38
|
-
import re
|
|
39
|
-
import sys
|
|
40
|
-
from pathlib import Path
|
|
41
|
-
|
|
42
|
-
# Repo root resolved from this file's location (scripts/ -> repo root). Used to
|
|
43
|
-
# anchor the default source / output paths so the migration is CWD-independent.
|
|
44
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
45
|
-
|
|
46
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
47
|
-
from _content_root import content_root # noqa: E402
|
|
48
|
-
|
|
49
|
-
# Shippable content moved under content/ in the source repo and is
|
|
50
|
-
# flattened to the framework root in a consumer deposit (#1875 C1).
|
|
51
|
-
CONTENT_ROOT = content_root(REPO_ROOT)
|
|
52
|
-
DEFAULT_SOURCE = REPO_ROOT / "meta" / "lessons.md"
|
|
53
|
-
DEFAULT_OUT = CONTENT_ROOT / "packs" / "lessons" / "lessons-pack-0.1.json"
|
|
54
|
-
|
|
55
|
-
PACK_ID = "lessons-pack-0.1"
|
|
56
|
-
PACK_VERSION = "0.1"
|
|
57
|
-
|
|
58
|
-
# Controlled tag vocabulary. MUST stay in lockstep with the `enum` and
|
|
59
|
-
# `x-tagVocabulary` in vbrief/schemas/lessons-pack.schema.json.
|
|
60
|
-
TAG_VOCABULARY: tuple[str, ...] = (
|
|
61
|
-
"windows",
|
|
62
|
-
"encoding",
|
|
63
|
-
"review-cycle",
|
|
64
|
-
"swarm",
|
|
65
|
-
"release",
|
|
66
|
-
"github",
|
|
67
|
-
"context",
|
|
68
|
-
"debugging",
|
|
69
|
-
"lifecycle",
|
|
70
|
-
"powershell",
|
|
71
|
-
"ci",
|
|
72
|
-
"agent-orchestration",
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
# Keyword -> tag scoring table. Matches are lowercase substring tests against
|
|
76
|
-
# the title (weighted heavily) and body (weighted lightly). The tokens are
|
|
77
|
-
# deliberately specific to keep the controlled vocabulary discriminating.
|
|
78
|
-
TAG_KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
79
|
-
"windows": ("windows", "cp1252", "cp437", "charmap", "win32", "winerror"),
|
|
80
|
-
"encoding": (
|
|
81
|
-
"encoding",
|
|
82
|
-
"utf-8",
|
|
83
|
-
"utf8",
|
|
84
|
-
"mojibake",
|
|
85
|
-
"non-ascii",
|
|
86
|
-
"u+fffd",
|
|
87
|
-
" bom",
|
|
88
|
-
"unicodedecode",
|
|
89
|
-
"unicodeencode",
|
|
90
|
-
),
|
|
91
|
-
"review-cycle": (
|
|
92
|
-
"review cycle",
|
|
93
|
-
"review-cycle",
|
|
94
|
-
"greptile",
|
|
95
|
-
"review bot",
|
|
96
|
-
"checkrun",
|
|
97
|
-
"check run",
|
|
98
|
-
),
|
|
99
|
-
"swarm": ("swarm", "parallel agent", "worktree", "cohort", "cascade"),
|
|
100
|
-
"release": ("release", "changelog", "publish", "v0.", "tag time", "cut session"),
|
|
101
|
-
"github": (
|
|
102
|
-
"github",
|
|
103
|
-
"gh api",
|
|
104
|
-
"gh cli",
|
|
105
|
-
"gh issue",
|
|
106
|
-
"gh pr",
|
|
107
|
-
"graphql",
|
|
108
|
-
"rest",
|
|
109
|
-
"pull request",
|
|
110
|
-
"closingissues",
|
|
111
|
-
),
|
|
112
|
-
"context": ("context engineering", "context rot", "context window", "token", "low-signal"),
|
|
113
|
-
"debugging": (
|
|
114
|
-
"debug",
|
|
115
|
-
"root cause",
|
|
116
|
-
"root-cause",
|
|
117
|
-
"investigation",
|
|
118
|
-
"forensic",
|
|
119
|
-
"blind spot",
|
|
120
|
-
),
|
|
121
|
-
"lifecycle": (
|
|
122
|
-
"lifecycle",
|
|
123
|
-
"vbrief",
|
|
124
|
-
"scope:",
|
|
125
|
-
"promote",
|
|
126
|
-
"activate",
|
|
127
|
-
"reconcile",
|
|
128
|
-
),
|
|
129
|
-
"powershell": (
|
|
130
|
-
"powershell",
|
|
131
|
-
"pwsh",
|
|
132
|
-
"ps 5.1",
|
|
133
|
-
"ps5.1",
|
|
134
|
-
"get-content",
|
|
135
|
-
"set-content",
|
|
136
|
-
"here-string",
|
|
137
|
-
),
|
|
138
|
-
"ci": ("pre-commit", "task check", "deterministic gate", " gate ", "pipeline", "self-test"),
|
|
139
|
-
"agent-orchestration": (
|
|
140
|
-
"orchestrat",
|
|
141
|
-
"poller",
|
|
142
|
-
"dispatch",
|
|
143
|
-
"sub-agent",
|
|
144
|
-
"subagent",
|
|
145
|
-
"agent run",
|
|
146
|
-
"spawn",
|
|
147
|
-
"monitor agent",
|
|
148
|
-
),
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
# Fallback tag when no keyword scores -- every entry must carry >= 1 tag.
|
|
152
|
-
FALLBACK_TAG = "agent-orchestration"
|
|
153
|
-
|
|
154
|
-
_HEADING_RE = re.compile(r"^## (.+)$")
|
|
155
|
-
_DATE_RE = re.compile(r"(\d{4}-\d{2})(?:-\d{2})?")
|
|
156
|
-
_ISSUE_RE = re.compile(r"#(\d+)")
|
|
157
|
-
_SOURCE_RE = re.compile(r"^\*\*Source:\*\*\s*(.+?)\s*$")
|
|
158
|
-
_SLUG_STRIP_RE = re.compile(r"[^a-z0-9]+")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def extract_date(title: str) -> str | None:
|
|
162
|
-
"""Return the YYYY-MM date from a heading, or None when absent."""
|
|
163
|
-
match = _DATE_RE.search(title)
|
|
164
|
-
return match.group(1) if match else None
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def extract_issue_refs(title: str) -> list[str]:
|
|
168
|
-
"""Return all ``#NNN`` references in a heading, in order of appearance."""
|
|
169
|
-
return [f"#{n}" for n in _ISSUE_RE.findall(title)]
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def extract_source(body: str) -> str | None:
|
|
173
|
-
"""Return the text of the first ``**Source:**`` line in the body, or None."""
|
|
174
|
-
for line in body.splitlines():
|
|
175
|
-
match = _SOURCE_RE.match(line.strip())
|
|
176
|
-
if match:
|
|
177
|
-
return match.group(1)
|
|
178
|
-
return None
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def slugify(title: str, existing: set[str]) -> str:
|
|
182
|
-
"""Derive a stable, unique, lowercase slug from a title.
|
|
183
|
-
|
|
184
|
-
Strips a trailing parenthetical date / issue ref before slugifying so the
|
|
185
|
-
id stays readable, then de-duplicates against ``existing`` by appending a
|
|
186
|
-
numeric suffix.
|
|
187
|
-
"""
|
|
188
|
-
base = _SLUG_STRIP_RE.sub("-", title.lower()).strip("-")
|
|
189
|
-
if not base:
|
|
190
|
-
base = "lesson"
|
|
191
|
-
slug = base
|
|
192
|
-
counter = 2
|
|
193
|
-
while slug in existing:
|
|
194
|
-
slug = f"{base}-{counter}"
|
|
195
|
-
counter += 1
|
|
196
|
-
existing.add(slug)
|
|
197
|
-
return slug
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def assign_tags(title: str, body: str) -> list[str]:
|
|
201
|
-
"""Assign 1-3 controlled-vocabulary tags by keyword scoring.
|
|
202
|
-
|
|
203
|
-
Title matches weigh 5x body matches. The top-scoring tags (score > 0) are
|
|
204
|
-
returned, capped at 3, ordered by score desc then by vocabulary order for
|
|
205
|
-
determinism. Falls back to a single default tag when nothing scores.
|
|
206
|
-
"""
|
|
207
|
-
title_l = title.lower()
|
|
208
|
-
body_l = body.lower()
|
|
209
|
-
scores: dict[str, int] = {}
|
|
210
|
-
for tag in TAG_VOCABULARY:
|
|
211
|
-
score = 0
|
|
212
|
-
for kw in TAG_KEYWORDS[tag]:
|
|
213
|
-
if kw in title_l:
|
|
214
|
-
score += 5
|
|
215
|
-
score += body_l.count(kw)
|
|
216
|
-
if score > 0:
|
|
217
|
-
scores[tag] = score
|
|
218
|
-
|
|
219
|
-
if not scores:
|
|
220
|
-
return [FALLBACK_TAG]
|
|
221
|
-
|
|
222
|
-
vocab_order = {tag: i for i, tag in enumerate(TAG_VOCABULARY)}
|
|
223
|
-
ranked = sorted(scores.items(), key=lambda kv: (-kv[1], vocab_order[kv[0]]))
|
|
224
|
-
return [tag for tag, _ in ranked[:3]]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def parse_lessons(md_text: str) -> list[dict]:
|
|
228
|
-
"""Parse lessons markdown into structured lesson entries.
|
|
229
|
-
|
|
230
|
-
Splits on top-level ``## `` headings; content before the first heading is
|
|
231
|
-
discarded (renderer-owned chrome). Returns entries in document order.
|
|
232
|
-
"""
|
|
233
|
-
lines = md_text.splitlines()
|
|
234
|
-
# Collect (heading_text, start_line_index_of_body) for each section.
|
|
235
|
-
sections: list[tuple[str, int]] = []
|
|
236
|
-
for idx, line in enumerate(lines):
|
|
237
|
-
match = _HEADING_RE.match(line)
|
|
238
|
-
if match:
|
|
239
|
-
sections.append((match.group(1).strip(), idx))
|
|
240
|
-
|
|
241
|
-
entries: list[dict] = []
|
|
242
|
-
existing_ids: set[str] = set()
|
|
243
|
-
for s_idx, (title, head_line) in enumerate(sections):
|
|
244
|
-
end_line = sections[s_idx + 1][1] if s_idx + 1 < len(sections) else len(lines)
|
|
245
|
-
body = "\n".join(lines[head_line + 1 : end_line]).strip()
|
|
246
|
-
entries.append(
|
|
247
|
-
{
|
|
248
|
-
"id": slugify(title, existing_ids),
|
|
249
|
-
"title": title,
|
|
250
|
-
"date": extract_date(title),
|
|
251
|
-
"issue_refs": extract_issue_refs(title),
|
|
252
|
-
"tags": assign_tags(title, body),
|
|
253
|
-
"source": extract_source(body),
|
|
254
|
-
"body": body,
|
|
255
|
-
}
|
|
256
|
-
)
|
|
257
|
-
return entries
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def build_pack(md_text: str, generated_from: str) -> dict:
|
|
261
|
-
"""Build the full pack object from the source markdown text."""
|
|
262
|
-
return {
|
|
263
|
-
"pack": PACK_ID,
|
|
264
|
-
"version": PACK_VERSION,
|
|
265
|
-
"generated_from": generated_from,
|
|
266
|
-
"lessons": parse_lessons(md_text),
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def migrate(source: Path, out: Path) -> dict:
|
|
271
|
-
"""Read ``source`` markdown, build the pack, and write it to ``out``.
|
|
272
|
-
|
|
273
|
-
Returns the in-memory pack object. Raises ``FileNotFoundError`` when the
|
|
274
|
-
source is missing and ``ValueError`` when it is empty.
|
|
275
|
-
"""
|
|
276
|
-
if not source.is_file():
|
|
277
|
-
raise FileNotFoundError(f"source markdown not found: {source}")
|
|
278
|
-
md_text = source.read_text(encoding="utf-8")
|
|
279
|
-
if not md_text.strip():
|
|
280
|
-
raise ValueError(f"source markdown is empty: {source}")
|
|
281
|
-
|
|
282
|
-
# Record provenance as a repo-relative path when possible.
|
|
283
|
-
try:
|
|
284
|
-
generated_from = source.resolve().relative_to(REPO_ROOT).as_posix()
|
|
285
|
-
except ValueError:
|
|
286
|
-
generated_from = source.name
|
|
287
|
-
|
|
288
|
-
pack = build_pack(md_text, generated_from)
|
|
289
|
-
out.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
-
# ensure_ascii=True: the canonical source is serialized as pure ASCII with
|
|
291
|
-
# \uXXXX escapes. This is lossless (json.loads reconstructs the exact same
|
|
292
|
-
# strings, so the rendered projection and slice output are byte-identical),
|
|
293
|
-
# and it keeps the source clean against `task verify:encoding` (#798): the
|
|
294
|
-
# lessons content legitimately documents cp1252/cp437 mojibake example
|
|
295
|
-
# tokens (e.g. the corrupted form of the U+2297 glyph), which the encoding
|
|
296
|
-
# gate's markdown inline-code stripping exempts in meta/lessons.md but
|
|
297
|
-
# would flag as a raw bigram in the JSON. Escaping sidesteps that without
|
|
298
|
-
# mutating the preserved content.
|
|
299
|
-
out.write_text(json.dumps(pack, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
|
300
|
-
return pack
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def main(argv: list[str] | None = None) -> int:
|
|
304
|
-
parser = argparse.ArgumentParser(
|
|
305
|
-
prog="pack_migrate_lessons.py",
|
|
306
|
-
description="Migrate meta/lessons.md into the structured lessons-pack-0.1 source.",
|
|
307
|
-
)
|
|
308
|
-
parser.add_argument(
|
|
309
|
-
"--source",
|
|
310
|
-
type=Path,
|
|
311
|
-
default=DEFAULT_SOURCE,
|
|
312
|
-
help="Source markdown to parse (default: meta/lessons.md).",
|
|
313
|
-
)
|
|
314
|
-
parser.add_argument(
|
|
315
|
-
"--out",
|
|
316
|
-
type=Path,
|
|
317
|
-
default=DEFAULT_OUT,
|
|
318
|
-
help="Output pack JSON path (default: packs/lessons/lessons-pack-0.1.json).",
|
|
319
|
-
)
|
|
320
|
-
args = parser.parse_args(argv)
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
pack = migrate(args.source, args.out)
|
|
324
|
-
except FileNotFoundError as exc:
|
|
325
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
326
|
-
return 1
|
|
327
|
-
except ValueError as exc:
|
|
328
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
329
|
-
return 1
|
|
330
|
-
|
|
331
|
-
print(f"Migrated {len(pack['lessons'])} lessons -> {args.out}")
|
|
332
|
-
return 0
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if __name__ == "__main__":
|
|
336
|
-
raise SystemExit(main())
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""pack_migrate_patterns.py -- one-shot migration: patterns/*.md -> pack.
|
|
3
|
-
|
|
4
|
-
Builds the canonical structured source ``packs/patterns/patterns-pack-0.1.json``
|
|
5
|
-
(the source of truth per ADR-001) by scanning every ``patterns/*.md``. This is
|
|
6
|
-
the #1637 generalization of the #1294 lessons pilot + #1295 skills pack + #1296
|
|
7
|
-
rules/strategies packs: the same render/slice machinery, a fifth domain. The
|
|
8
|
-
``patterns/`` directory existed with no pack until #1637 (packs:slice v2).
|
|
9
|
-
|
|
10
|
-
What is captured per pattern
|
|
11
|
-
----------------------------
|
|
12
|
-
- ``id`` the slugified doc stem (e.g. multi-agent, role-as-overlay).
|
|
13
|
-
- ``title`` the leading ``# `` heading text, verbatim.
|
|
14
|
-
- ``description`` the leading description paragraph after the title, folded to a
|
|
15
|
-
single normalised string (Legend / See-also / HTML-comment / rule chrome
|
|
16
|
-
skipped). Empty when the doc has no leading paragraph.
|
|
17
|
-
- ``triggers`` invocation keywords for the pattern. Pattern docs carry no
|
|
18
|
-
frontmatter and there is no pattern-routing table, so the derivable trigger
|
|
19
|
-
is the doc stem itself; the list is otherwise empty (mirrors #1296 strategies
|
|
20
|
-
scope: "if no trigger metadata exists, use an empty list and rely on list").
|
|
21
|
-
- ``path`` the repo-relative ``patterns/<name>.md``.
|
|
22
|
-
- ``body`` the full pattern body (banner-stripped) for the ONE designated
|
|
23
|
-
proof pattern (``patterns/multi-agent.md``); ``null`` for every other pattern
|
|
24
|
-
(metadata-only, per the "migrate ONE doc as proof" 0.1-pilot scope).
|
|
25
|
-
|
|
26
|
-
Usage::
|
|
27
|
-
|
|
28
|
-
uv run python scripts/pack_migrate_patterns.py \\
|
|
29
|
-
[--patterns-dir patterns] [--proof-pattern patterns/multi-agent.md] \\
|
|
30
|
-
[--out packs/patterns/patterns-pack-0.1.json]
|
|
31
|
-
|
|
32
|
-
Exit codes:
|
|
33
|
-
0 -- migrated successfully
|
|
34
|
-
1 -- patterns dir missing, or no patterns discovered
|
|
35
|
-
2 -- usage error
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
from __future__ import annotations
|
|
39
|
-
|
|
40
|
-
import argparse
|
|
41
|
-
import json
|
|
42
|
-
import re
|
|
43
|
-
import sys
|
|
44
|
-
from pathlib import Path
|
|
45
|
-
|
|
46
|
-
# Repo root resolved from this file's location (scripts/ -> repo root) so the
|
|
47
|
-
# default paths are CWD-independent.
|
|
48
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
49
|
-
|
|
50
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
51
|
-
from _content_root import content_root # noqa: E402
|
|
52
|
-
|
|
53
|
-
# Shippable content moved under content/ in the source repo and is
|
|
54
|
-
# flattened to the framework root in a consumer deposit (#1875 C1).
|
|
55
|
-
CONTENT_ROOT = content_root(REPO_ROOT)
|
|
56
|
-
DEFAULT_PATTERNS_DIR = CONTENT_ROOT / "patterns"
|
|
57
|
-
DEFAULT_OUT = CONTENT_ROOT / "packs" / "patterns" / "patterns-pack-0.1.json"
|
|
58
|
-
|
|
59
|
-
PACK_ID = "patterns-pack-0.1"
|
|
60
|
-
PACK_VERSION = "0.1"
|
|
61
|
-
|
|
62
|
-
# The single proof PATTERN whose full body is captured + regenerated as a
|
|
63
|
-
# banner-marked, drift-checked projection.
|
|
64
|
-
DEFAULT_PROOF_PATTERN = "patterns/multi-agent.md"
|
|
65
|
-
|
|
66
|
-
_H1_RE = re.compile(r"^#\s+(.+?)\s*$")
|
|
67
|
-
_SLUG_STRIP_RE = re.compile(r"[^a-z0-9]+")
|
|
68
|
-
|
|
69
|
-
# Lines that open the description-paragraph scan but are document chrome, not a
|
|
70
|
-
# description: the RFC2119 legend, "See also" pointers, HTML comments, and
|
|
71
|
-
# horizontal rules.
|
|
72
|
-
_CHROME_PREFIXES = ("legend ", "legend(", "**legend", "**⚠️", "**see also", "<!--")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _is_chrome(line: str) -> bool:
|
|
76
|
-
"""True when a line is document chrome rather than a description paragraph."""
|
|
77
|
-
low = line.lstrip().lower()
|
|
78
|
-
if low.startswith(_CHROME_PREFIXES):
|
|
79
|
-
return True
|
|
80
|
-
stripped = line.strip()
|
|
81
|
-
# A horizontal rule (e.g. `---`, `===`) is chrome, not a description.
|
|
82
|
-
return bool(stripped) and set(stripped) <= {"-", "="}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def extract_title(md_text: str) -> str:
|
|
86
|
-
"""Return the leading ``# `` heading text, or '' when absent."""
|
|
87
|
-
for line in md_text.splitlines():
|
|
88
|
-
match = _H1_RE.match(line)
|
|
89
|
-
if match:
|
|
90
|
-
return match.group(1).strip()
|
|
91
|
-
return ""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def extract_description(md_text: str) -> str:
|
|
95
|
-
"""Return the leading description paragraph after the ``# `` title.
|
|
96
|
-
|
|
97
|
-
Scans past the title, skips blank lines and chrome lines (Legend, See-also,
|
|
98
|
-
HTML comments, horizontal rules), then collects the first contiguous block
|
|
99
|
-
of non-blank, non-chrome lines and folds it to a single normalised string.
|
|
100
|
-
A leading blockquote marker (``> ``) is stripped so redirect/superseded
|
|
101
|
-
notes still yield a readable description.
|
|
102
|
-
"""
|
|
103
|
-
lines = md_text.splitlines()
|
|
104
|
-
i = 0
|
|
105
|
-
n = len(lines)
|
|
106
|
-
# Advance to just past the first H1.
|
|
107
|
-
while i < n and not _H1_RE.match(lines[i]):
|
|
108
|
-
i += 1
|
|
109
|
-
if i < n:
|
|
110
|
-
i += 1 # skip the title line itself
|
|
111
|
-
# Skip leading blanks / chrome before the description.
|
|
112
|
-
while i < n and (lines[i].strip() == "" or _is_chrome(lines[i])):
|
|
113
|
-
i += 1
|
|
114
|
-
# Collect the first contiguous non-blank block. A markdown heading is a
|
|
115
|
-
# section boundary, not a description -- a doc whose first content after the
|
|
116
|
-
# title is a `## ` heading has no leading description paragraph.
|
|
117
|
-
block: list[str] = []
|
|
118
|
-
while i < n and lines[i].strip() != "" and not lines[i].lstrip().startswith("#"):
|
|
119
|
-
stripped = lines[i].strip()
|
|
120
|
-
if stripped.startswith(">"):
|
|
121
|
-
stripped = stripped.lstrip(">").strip()
|
|
122
|
-
if stripped:
|
|
123
|
-
block.append(stripped)
|
|
124
|
-
i += 1
|
|
125
|
-
return " ".join(block)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def strip_leading_banner(body: str) -> str:
|
|
129
|
-
"""Strip a leading provenance banner + blank lines from a captured body.
|
|
130
|
-
|
|
131
|
-
Makes re-migration idempotent: after the proof pattern is regenerated
|
|
132
|
-
(banner + body), re-running the migration recovers the same body. Only
|
|
133
|
-
strips a banner block that opens with the renderer's first banner line, so
|
|
134
|
-
unrelated leading HTML comments survive.
|
|
135
|
-
"""
|
|
136
|
-
lines = body.split("\n")
|
|
137
|
-
i = 0
|
|
138
|
-
while i < len(lines) and lines[i].strip() == "":
|
|
139
|
-
i += 1
|
|
140
|
-
if i < len(lines) and lines[i].startswith(
|
|
141
|
-
"<!-- AUTO-GENERATED by task packs:render"
|
|
142
|
-
):
|
|
143
|
-
while i < len(lines) and lines[i].lstrip().startswith("<!--"):
|
|
144
|
-
i += 1
|
|
145
|
-
while i < len(lines) and lines[i].strip() == "":
|
|
146
|
-
i += 1
|
|
147
|
-
return "\n".join(lines[i:])
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def build_pattern_entry(
|
|
151
|
-
md: Path, patterns_dir: Path, *, capture_body: bool
|
|
152
|
-
) -> dict:
|
|
153
|
-
"""Build one pattern entry from its markdown file."""
|
|
154
|
-
rel_path = md.resolve().relative_to(patterns_dir.resolve().parent).as_posix()
|
|
155
|
-
stem_slug = _SLUG_STRIP_RE.sub("-", md.stem.lower()).strip("-")
|
|
156
|
-
text = md.read_text(encoding="utf-8")
|
|
157
|
-
return {
|
|
158
|
-
"id": stem_slug,
|
|
159
|
-
"title": extract_title(text),
|
|
160
|
-
"description": extract_description(text),
|
|
161
|
-
"triggers": [stem_slug] if stem_slug else [],
|
|
162
|
-
"path": rel_path,
|
|
163
|
-
"body": strip_leading_banner(text) if capture_body else None,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def build_pack(patterns_dir: Path, *, proof_pattern: str) -> dict:
|
|
168
|
-
"""Scan the patterns dir and assemble the full pack object."""
|
|
169
|
-
patterns: list[dict] = []
|
|
170
|
-
for md in sorted(patterns_dir.glob("*.md")):
|
|
171
|
-
rel_path = md.resolve().relative_to(patterns_dir.resolve().parent).as_posix()
|
|
172
|
-
patterns.append(
|
|
173
|
-
build_pattern_entry(
|
|
174
|
-
md, patterns_dir, capture_body=(rel_path == proof_pattern)
|
|
175
|
-
)
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
"pack": PACK_ID,
|
|
180
|
-
"version": PACK_VERSION,
|
|
181
|
-
"generated_from": "patterns/*.md",
|
|
182
|
-
"patterns": patterns,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def migrate(patterns_dir: Path, out: Path, *, proof_pattern: str) -> dict:
|
|
187
|
-
"""Build the pack from ``patterns_dir`` and write it to ``out``.
|
|
188
|
-
|
|
189
|
-
Raises ``FileNotFoundError`` when the dir is missing and ``ValueError`` when
|
|
190
|
-
no patterns are discovered.
|
|
191
|
-
"""
|
|
192
|
-
if not patterns_dir.is_dir():
|
|
193
|
-
raise FileNotFoundError(f"patterns directory not found: {patterns_dir}")
|
|
194
|
-
|
|
195
|
-
pack = build_pack(patterns_dir, proof_pattern=proof_pattern)
|
|
196
|
-
if not pack["patterns"]:
|
|
197
|
-
raise ValueError(f"no patterns discovered under {patterns_dir}")
|
|
198
|
-
|
|
199
|
-
out.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
-
# ensure_ascii=True: the canonical source is serialized as pure ASCII with
|
|
201
|
-
# \uXXXX escapes (mirrors the other pack migrations). Lossless and keeps the
|
|
202
|
-
# JSON clean against `task verify:encoding` (#798) even when a pattern body
|
|
203
|
-
# carries non-ASCII glyphs (em dashes, RFC2119 symbols, emoji in diagrams).
|
|
204
|
-
out.write_text(
|
|
205
|
-
json.dumps(pack, indent=2, ensure_ascii=True) + "\n", encoding="utf-8"
|
|
206
|
-
)
|
|
207
|
-
return pack
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def main(argv: list[str] | None = None) -> int:
|
|
211
|
-
parser = argparse.ArgumentParser(
|
|
212
|
-
prog="pack_migrate_patterns.py",
|
|
213
|
-
description="Migrate patterns/*.md into the patterns-pack-0.1 source.",
|
|
214
|
-
)
|
|
215
|
-
parser.add_argument(
|
|
216
|
-
"--patterns-dir",
|
|
217
|
-
type=Path,
|
|
218
|
-
default=DEFAULT_PATTERNS_DIR,
|
|
219
|
-
help="Directory of pattern docs to scan (default: patterns/).",
|
|
220
|
-
)
|
|
221
|
-
parser.add_argument(
|
|
222
|
-
"--proof-pattern",
|
|
223
|
-
default=DEFAULT_PROOF_PATTERN,
|
|
224
|
-
help="Repo-relative path of the one pattern whose full body is captured.",
|
|
225
|
-
)
|
|
226
|
-
parser.add_argument(
|
|
227
|
-
"--out",
|
|
228
|
-
type=Path,
|
|
229
|
-
default=DEFAULT_OUT,
|
|
230
|
-
help="Output pack JSON path (default: packs/patterns/patterns-pack-0.1.json).",
|
|
231
|
-
)
|
|
232
|
-
args = parser.parse_args(argv)
|
|
233
|
-
|
|
234
|
-
try:
|
|
235
|
-
pack = migrate(
|
|
236
|
-
args.patterns_dir, args.out, proof_pattern=args.proof_pattern
|
|
237
|
-
)
|
|
238
|
-
except FileNotFoundError as exc:
|
|
239
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
240
|
-
return 1
|
|
241
|
-
except ValueError as exc:
|
|
242
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
243
|
-
return 1
|
|
244
|
-
|
|
245
|
-
bodied = sum(1 for s in pack["patterns"] if s["body"] is not None)
|
|
246
|
-
print(
|
|
247
|
-
f"Migrated {len(pack['patterns'])} patterns ({bodied} with body) "
|
|
248
|
-
f"-> {args.out}"
|
|
249
|
-
)
|
|
250
|
-
return 0
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if __name__ == "__main__":
|
|
254
|
-
raise SystemExit(main())
|