@deftai/directive-core 0.84.0 → 0.86.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.
Files changed (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -0,0 +1,708 @@
1
+ // Directive Rule Map generator — maintainer documentation of the rule taxonomy.
2
+ //
3
+ // Produces two artifacts from the repo's own source of truth (content/ + tasks/):
4
+ // docs/RULE-MAP.md committed, diff-friendly Markdown map (no timestamps)
5
+ // docs/rule-map/index.html gitignored, self-contained interactive explorer
6
+ // (zero runtime dependencies — data + source bodies
7
+ // inlined; opens straight from file://)
8
+ //
9
+ // Internal documentation for maintainers and rule/pack authors. It is not shipped
10
+ // in the npm/content payload (it lives under the maintainer-only top-level docs/).
11
+ //
12
+ // Node standard library only — no external dependencies, no CDN, no network.
13
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
+ import { basename, dirname, join, relative, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
17
+ import { TEMPLATE } from "./rule-map-template.js";
18
+ const MARKER_RE = /^\s*[-*]\s*([!~⊗≉?])\s/;
19
+ const MARKER_LABEL = {
20
+ "!": "MUST",
21
+ "~": "SHOULD",
22
+ "⊗": "MUST NOT",
23
+ "≉": "SHOULD NOT",
24
+ "?": "MAY",
25
+ };
26
+ const MAX_BODY = 300_000; // cap per-file body inlined into the HTML (content docs are small)
27
+ const EXCLUDE_GROUPS = new Set(["packs", "secrets"]);
28
+ const GROUP_PURPOSE = {
29
+ coding: "Core software-development rules for agents: hygiene, testing, debugging, security, build output.",
30
+ languages: "Per-language standards and idioms, loaded lazily when the task touches that language.",
31
+ skills: "Packaged multi-step agent workflows (build, release, interview, triage, review…).",
32
+ strategies: "Higher-order approaches: interviewing, decomposition, planning, research, refactoring.",
33
+ deployments: "Provider-specific deployment playbooks (AWS, Azure, GCP, Cloudflare, Vercel, fly.io…).",
34
+ vbrief: "The durable state format: project definition, specification, scopes, plans.",
35
+ scm: "Source-control conventions and Git/GitHub workflow rules.",
36
+ verification: "How agents prove work is done: gates, validators, coverage, review.",
37
+ tools: "Tooling standards (telemetry, search, formatters, the Taskfile contract).",
38
+ patterns: "Reusable design/interaction patterns for agent work.",
39
+ conventions: "Cross-cutting naming, formatting, and repo conventions.",
40
+ context: "How to feed agents context well: examples, deterministic splits, spec deltas.",
41
+ contracts: "Interface/behavioral contracts the framework enforces.",
42
+ events: "Event and signal definitions used across the framework.",
43
+ interfaces: "Interface definitions and boundaries.",
44
+ platforms: "Platform-specific guidance.",
45
+ resilience: "Failure handling, recovery, and robustness rules.",
46
+ incidents: "Incident handling and postmortem guidance.",
47
+ secrets: "Secret-handling rules and storage conventions.",
48
+ swarm: "Multi-agent (swarm) coordination guidance.",
49
+ references: "External references and citations backing the guidance.",
50
+ meta: "The framework's own philosophy, morals, and self-governance docs.",
51
+ templates: "Reusable document/scaffold templates.",
52
+ docs: "Explanatory docs and the framework glossary.",
53
+ };
54
+ const NS_PURPOSE = {
55
+ scm: "Source-control / Git workflow tasks.",
56
+ commit: "Commit-creation and message-discipline tasks.",
57
+ change: "Change-set staging and review tasks.",
58
+ install: "Installer / bootstrap tasks for consumer projects.",
59
+ migrate: "Migration tasks for moving projects between framework versions.",
60
+ roadmap: "Render and validate the ROADMAP from vBRIEF source.",
61
+ spec: "Specification render/validate tasks (vBRIEF -> SPECIFICATION).",
62
+ pr: "PR-level merge-discipline checks.",
63
+ scope: "Scope lifecycle: promote / activate / complete / fail / cancel.",
64
+ vbrief: "Validate and manage vBRIEF lifecycle state and structure.",
65
+ verify: "Verification gates: stub scans, session ritual, story-ready, oracles.",
66
+ eval: "Self-consistency and quality evaluation tasks.",
67
+ };
68
+ const LIFECYCLE = {
69
+ summary: "Directive turns a coding agent into an auditable process: load only the guidance a task needs, enforce it with Taskfile gates, keep durable state in vBRIEF, and move work through a small reversible scope lifecycle.",
70
+ rule_strength: [
71
+ { level: "Deterministic checks", detail: "tests, scripts, hooks, CI" },
72
+ { level: "Taskfile targets", detail: "task check, task verify:*, task vbrief:*" },
73
+ { level: "vBRIEF policy", detail: "lifecycle metadata" },
74
+ { level: "RFC2119 instructions", detail: "AGENTS.md, skills, standards" },
75
+ { level: "Plain prose", detail: "rationale only" },
76
+ ],
77
+ scope_states: [
78
+ { state: "proposed", via: "candidate" },
79
+ { state: "pending", via: "scope:promote" },
80
+ { state: "active", via: "scope:activate" },
81
+ { state: "completed", via: "scope:complete" },
82
+ { state: "cancelled", via: "scope:cancel / scope:fail" },
83
+ ],
84
+ vbrief_files: [
85
+ { file: "PROJECT-DEFINITION.vbrief.json", role: "project identity, policy, scope registry" },
86
+ { file: "specification.vbrief.json", role: "project specification source" },
87
+ { file: "plan.vbrief.json", role: "tactical session plan" },
88
+ { file: "continue.vbrief.json", role: "interruption recovery checkpoint" },
89
+ ],
90
+ gates: [
91
+ "task check",
92
+ "task check:framework-source",
93
+ "task verify:session-ritual",
94
+ "task vbrief:validate",
95
+ "task codebase:validate-structure",
96
+ ],
97
+ source: "docs/CONCEPTS.md",
98
+ key_docs: [
99
+ { title: "Key Concepts", path: "docs/CONCEPTS.md" },
100
+ { title: "Architecture", path: "docs/ARCHITECTURE.md" },
101
+ { title: "Files", path: "docs/FILES.md" },
102
+ { title: "README", path: "README.md" },
103
+ { title: "AGENTS.md", path: "AGENTS.md" },
104
+ { title: "main.md", path: "main.md" },
105
+ ],
106
+ };
107
+ // --------------------------------------------------------------- text helpers
108
+ function read(repo, rel) {
109
+ try {
110
+ return readFileSync(join(repo, rel), "utf8");
111
+ }
112
+ catch {
113
+ return "";
114
+ }
115
+ }
116
+ function h1(t) {
117
+ for (const line of t.split("\n")) {
118
+ if (line.startsWith("# "))
119
+ return line.slice(2).trim();
120
+ }
121
+ return "";
122
+ }
123
+ function clamp(p, hi) {
124
+ if (p.length <= hi)
125
+ return p;
126
+ const t = p.slice(0, hi - 3);
127
+ const i = t.lastIndexOf(" ");
128
+ return `${i > 0 ? t.slice(0, i) : t}…`;
129
+ }
130
+ function firstPara(t) {
131
+ const buf = [];
132
+ const skip = ["#", ">", "<!--", "**⚠", "Legend", "- [", "|"];
133
+ for (const line of t.split("\n")) {
134
+ const s = line.trim();
135
+ if (!s) {
136
+ if (buf.length)
137
+ break;
138
+ continue;
139
+ }
140
+ if (skip.some((p) => s.startsWith(p)) || MARKER_RE.test(line) || s.startsWith("- ")) {
141
+ if (buf.length)
142
+ break;
143
+ continue;
144
+ }
145
+ buf.push(s);
146
+ if (buf.join(" ").length > 220)
147
+ break;
148
+ }
149
+ const p = buf.join(" ").replace(/\s+/g, " ").trim();
150
+ return clamp(p, 240);
151
+ }
152
+ function sections(t) {
153
+ return t
154
+ .split("\n")
155
+ .filter((l) => l.startsWith("## "))
156
+ .map((l) => l.slice(3).trim())
157
+ .slice(0, 12);
158
+ }
159
+ function isGenerated(t) {
160
+ return t.slice(0, 400).includes("AUTO-GENERATED");
161
+ }
162
+ function markers(t) {
163
+ const c = {};
164
+ for (const line of t.split("\n")) {
165
+ const m = line.match(MARKER_RE);
166
+ if (m)
167
+ c[m[1]] = (c[m[1]] ?? 0) + 1;
168
+ }
169
+ return c;
170
+ }
171
+ /** Normalize repo-relative paths to `/` so Markdown/HTML output is OS-stable. */
172
+ function normRel(p) {
173
+ return p.split("\\").join("/");
174
+ }
175
+ function walkMd(dir) {
176
+ const out = [];
177
+ for (const e of readdirSync(dir).sort()) {
178
+ const full = join(dir, e);
179
+ if (statSync(full).isDirectory())
180
+ out.push(...walkMd(full));
181
+ else if (e.endsWith(".md"))
182
+ out.push(full);
183
+ }
184
+ return out;
185
+ }
186
+ function walkJson(dir) {
187
+ const out = [];
188
+ if (!existsSync(dir))
189
+ return out;
190
+ for (const e of readdirSync(dir).sort()) {
191
+ const full = join(dir, e);
192
+ if (statSync(full).isDirectory())
193
+ out.push(...walkJson(full));
194
+ else if (e.endsWith(".json"))
195
+ out.push(full);
196
+ }
197
+ return out;
198
+ }
199
+ // --------------------------------------------------------------- model build
200
+ function summarizeFile(repo, rel, withBody = true) {
201
+ const path = normRel(rel);
202
+ const t = read(repo, path);
203
+ const d = {
204
+ kind: "file",
205
+ name: basename(path),
206
+ title: h1(t) || basename(path),
207
+ summary: firstPara(t),
208
+ sections: sections(t),
209
+ markers: markers(t),
210
+ generated: isGenerated(t),
211
+ lines: t ? t.split("\n").length : 0,
212
+ path,
213
+ };
214
+ if (withBody) {
215
+ d.body = t.slice(0, MAX_BODY);
216
+ d.truncated = t.length > MAX_BODY;
217
+ }
218
+ return d;
219
+ }
220
+ function summarizeDir(repo, reldir) {
221
+ const dir = normRel(reldir);
222
+ let readme = null;
223
+ const files = [];
224
+ for (const entry of readdirSync(join(repo, dir)).sort()) {
225
+ const full = normRel(join(dir, entry));
226
+ const ab = join(repo, full);
227
+ if (statSync(ab).isDirectory()) {
228
+ for (const sub of walkMd(ab))
229
+ files.push(summarizeFile(repo, normRel(relative(repo, sub))));
230
+ }
231
+ else if (entry.endsWith(".md")) {
232
+ const fi = summarizeFile(repo, full);
233
+ if (entry.toLowerCase() === "readme.md")
234
+ readme = fi;
235
+ else
236
+ files.push(fi);
237
+ }
238
+ }
239
+ return {
240
+ kind: "dir",
241
+ name: basename(dir),
242
+ title: readme ? readme.title : basename(dir),
243
+ summary: readme ? readme.summary : "",
244
+ readme,
245
+ files,
246
+ count: files.length,
247
+ path: dir,
248
+ };
249
+ }
250
+ function addMarkers(totals, fi) {
251
+ for (const [k, v] of Object.entries(fi.markers))
252
+ totals[k] = (totals[k] ?? 0) + v;
253
+ }
254
+ function buildGroupings(repo) {
255
+ const groups = [];
256
+ const content = join(repo, "content");
257
+ for (const name of readdirSync(content).sort()) {
258
+ const gdir = join(content, name);
259
+ if (!statSync(gdir).isDirectory() || name.startsWith(".") || EXCLUDE_GROUPS.has(name))
260
+ continue;
261
+ const rel = normRel(join("content", name));
262
+ const items = [];
263
+ for (const entry of readdirSync(gdir).sort()) {
264
+ const full = normRel(join(rel, entry));
265
+ const ab = join(repo, full);
266
+ if (statSync(ab).isDirectory())
267
+ items.push(summarizeDir(repo, full));
268
+ else if (entry.endsWith(".md"))
269
+ items.push(summarizeFile(repo, full));
270
+ }
271
+ const totals = {};
272
+ for (const it of items) {
273
+ if (it.kind === "file")
274
+ addMarkers(totals, it);
275
+ else {
276
+ if (it.readme)
277
+ addMarkers(totals, it.readme);
278
+ for (const f of it.files)
279
+ addMarkers(totals, f);
280
+ }
281
+ }
282
+ const groupMarkers = {};
283
+ for (const [k, v] of Object.entries(totals)) {
284
+ const label = MARKER_LABEL[k];
285
+ if (label)
286
+ groupMarkers[label] = v;
287
+ }
288
+ const docCount = items.reduce((a, it) => a + (it.kind === "file" ? 1 : it.count + (it.readme ? 1 : 0)), 0);
289
+ groups.push({
290
+ name,
291
+ purpose: GROUP_PURPOSE[name] ?? "",
292
+ item_count: items.length,
293
+ doc_count: docCount,
294
+ markers: groupMarkers,
295
+ path: rel,
296
+ items,
297
+ });
298
+ }
299
+ return groups;
300
+ }
301
+ function parseIncludes(repo) {
302
+ // Normalize CRLF -> LF so the `:\n` include pattern matches Taskfiles saved with
303
+ // Windows line endings (or checked out with core.autocrlf=true).
304
+ const text = read(repo, "Taskfile.yml").replace(/\r\n/g, "\n");
305
+ const out = [];
306
+ const re = /^ {2}([a-z][a-z0-9-]*):\n\s+taskfile:\s*\.\/tasks\/(\S+)/gm;
307
+ let m = re.exec(text);
308
+ while (m) {
309
+ out.push({ namespace: m[1], file: m[2] });
310
+ m = re.exec(text);
311
+ }
312
+ return out;
313
+ }
314
+ function parseTasks(repo, relfile) {
315
+ const text = read(repo, relfile).replace(/\r\n/g, "\n");
316
+ const tasks = [];
317
+ const lines = text.split("\n");
318
+ let inTasks = false;
319
+ for (let i = 0; i < lines.length; i += 1) {
320
+ const line = lines[i];
321
+ if (/^tasks:\s*$/.test(line)) {
322
+ inTasks = true;
323
+ continue;
324
+ }
325
+ if (!inTasks)
326
+ continue;
327
+ const nameMatch = line.match(/^ {2}([a-zA-Z_][\w:-]*):\s*$/);
328
+ if (nameMatch) {
329
+ tasks.push({ name: nameMatch[1], desc: "" });
330
+ continue;
331
+ }
332
+ const descMatch = line.match(/^(\s+)desc:\s*["']?(.*?)["']?\s*$/);
333
+ const last = tasks[tasks.length - 1];
334
+ if (descMatch && last && !last.desc) {
335
+ let value = descMatch[2];
336
+ // YAML block scalars (`desc: >-`, `>`, `|`, `|-`) carry their text on the
337
+ // following more-indented lines; fold them into one line rather than
338
+ // recording the literal indicator token (e.g. ">-").
339
+ if (/^[>|][+-]?$/.test(value.trim())) {
340
+ const indent = descMatch[1].length;
341
+ const buf = [];
342
+ let j = i + 1;
343
+ while (j < lines.length) {
344
+ const bl = lines[j];
345
+ if (bl.trim() !== "") {
346
+ const ind = bl.length - bl.replace(/^\s+/, "").length;
347
+ if (ind <= indent)
348
+ break;
349
+ buf.push(bl.trim());
350
+ }
351
+ j += 1;
352
+ }
353
+ value = buf.join(" ").replace(/\s+/g, " ").trim();
354
+ i = j - 1;
355
+ }
356
+ last.desc = value;
357
+ }
358
+ }
359
+ const comments = [];
360
+ for (const line of text.split("\n")) {
361
+ const s = line.trim();
362
+ if (s.startsWith("#")) {
363
+ const c = s.replace(/^#+\s*/, "").trim();
364
+ if (c)
365
+ comments.push(c);
366
+ }
367
+ else if (s.startsWith("tasks:"))
368
+ break;
369
+ }
370
+ const bad = [
371
+ "Per ",
372
+ "joinPath",
373
+ "IMPORTANT",
374
+ "CLI_ARGS",
375
+ "env:",
376
+ "----",
377
+ "====",
378
+ "Keep ",
379
+ "templating",
380
+ "native-separator",
381
+ "STUB",
382
+ "PYTHONUTF8",
383
+ "Windows",
384
+ "pwsh",
385
+ ];
386
+ const clean = (c) => {
387
+ if (c.includes(" -- "))
388
+ return (c.split(" -- ")[1] ?? "").trim();
389
+ if (c.includes(" — "))
390
+ return (c.split(" — ")[1] ?? "").trim();
391
+ return c.trim();
392
+ };
393
+ let purpose = "";
394
+ for (const c of comments) {
395
+ if (bad.some((b) => c.includes(b)) || /^[-=# ]*$/.test(c))
396
+ continue;
397
+ purpose = clean(c);
398
+ if (purpose && purpose.length > 6)
399
+ break;
400
+ }
401
+ if (!purpose ||
402
+ (purpose[0] && purpose[0] === purpose[0].toLowerCase() && /[a-z]/.test(purpose[0]))) {
403
+ for (const t of tasks) {
404
+ if (t.desc) {
405
+ purpose = t.desc;
406
+ break;
407
+ }
408
+ }
409
+ }
410
+ return { purpose: clamp(purpose, 140), tasks };
411
+ }
412
+ function buildTasks(repo) {
413
+ const out = [];
414
+ const seen = new Set();
415
+ for (const inc of parseIncludes(repo)) {
416
+ const rel = normRel(join("tasks", inc.file));
417
+ if (!existsSync(join(repo, rel)))
418
+ continue;
419
+ const { purpose, tasks } = parseTasks(repo, rel);
420
+ out.push({
421
+ namespace: inc.namespace,
422
+ purpose: NS_PURPOSE[inc.namespace] ?? purpose,
423
+ task_count: tasks.length,
424
+ tasks: tasks.filter((t) => !t.name.startsWith("_")).slice(0, 40),
425
+ path: rel,
426
+ });
427
+ seen.add(inc.file);
428
+ }
429
+ const tasksDir = join(repo, "tasks");
430
+ if (!existsSync(tasksDir)) {
431
+ out.sort((a, b) => (a.namespace < b.namespace ? -1 : a.namespace > b.namespace ? 1 : 0));
432
+ return out;
433
+ }
434
+ for (const f of readdirSync(tasksDir).sort()) {
435
+ if (!f.endsWith(".yml") || seen.has(f))
436
+ continue;
437
+ const rel = normRel(join("tasks", f));
438
+ const { purpose, tasks } = parseTasks(repo, rel);
439
+ if (!tasks.length)
440
+ continue;
441
+ const ns = f.slice(0, -4);
442
+ out.push({
443
+ namespace: ns,
444
+ purpose: NS_PURPOSE[ns] ?? purpose,
445
+ task_count: tasks.length,
446
+ tasks: tasks.filter((t) => !t.name.startsWith("_")).slice(0, 40),
447
+ path: rel,
448
+ unlisted: true,
449
+ });
450
+ }
451
+ out.sort((a, b) => (a.namespace < b.namespace ? -1 : a.namespace > b.namespace ? 1 : 0));
452
+ return out;
453
+ }
454
+ function counter(values) {
455
+ const c = {};
456
+ for (const v of values) {
457
+ if (v === undefined)
458
+ continue; // don't pollute with an "undefined" key
459
+ c[v] = (c[v] ?? 0) + 1;
460
+ }
461
+ return c;
462
+ }
463
+ function buildPacks(repo) {
464
+ const packs = [];
465
+ for (const pj of walkJson(join(repo, "content", "packs"))) {
466
+ const rel = normRel(relative(repo, pj));
467
+ let d;
468
+ try {
469
+ const parsed = JSON.parse(readFileSync(pj, "utf8"));
470
+ // JSON.parse returns top-level null / primitives without throwing; only
471
+ // objects have the fields we read below.
472
+ if (parsed === null || typeof parsed !== "object")
473
+ continue;
474
+ d = parsed;
475
+ }
476
+ catch {
477
+ continue;
478
+ }
479
+ const info = {
480
+ path: rel,
481
+ name: d.pack ?? basename(pj),
482
+ version: d.version ?? "",
483
+ };
484
+ const rules = d.rules;
485
+ if (Array.isArray(rules)) {
486
+ info.rule_count = rules.length;
487
+ info.tiers = counter(rules.map((r) => r.tier));
488
+ info.domains = counter(rules.map((r) => r.domain));
489
+ }
490
+ packs.push(info);
491
+ }
492
+ return packs;
493
+ }
494
+ function stripBody(fi) {
495
+ fi.body = undefined;
496
+ fi.truncated = undefined;
497
+ }
498
+ function buildModel(repo, withBodies) {
499
+ const g = buildGroupings(repo);
500
+ if (!withBodies) {
501
+ for (const gr of g) {
502
+ for (const it of gr.items) {
503
+ if (it.kind === "file")
504
+ stripBody(it);
505
+ else {
506
+ if (it.readme)
507
+ stripBody(it.readme);
508
+ for (const f of it.files)
509
+ stripBody(f);
510
+ }
511
+ }
512
+ }
513
+ }
514
+ return { lifecycle: LIFECYCLE, groupings: g, tasks: buildTasks(repo), packs: buildPacks(repo) };
515
+ }
516
+ // --------------------------------------------------------------- MD rendering
517
+ function mdTable(headers, rows) {
518
+ // Collapse embedded newlines and escape `|` so cell values cannot break table structure
519
+ // (e.g. option syntax like `--format=text|json` in task descriptions).
520
+ // Escape backslashes first so a trailing `\` cannot neutralize the pipe escape (CodeQL js/incomplete-sanitization).
521
+ const cell = (c) => String(c).replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
522
+ const out = [
523
+ `| ${headers.map(cell).join(" | ")} |`,
524
+ `|${headers.map((_, i) => (i === 0 ? "---" : "--:")).join("|")}|`,
525
+ ];
526
+ for (const r of rows)
527
+ out.push(`| ${r.map(cell).join(" | ")} |`);
528
+ return out.join("\n");
529
+ }
530
+ function renderMd(model) {
531
+ const { groupings: g, tasks: tk, packs } = model;
532
+ const totalDocs = g.reduce((a, x) => a + x.doc_count, 0);
533
+ const totalTasks = tk.reduce((a, t) => a + t.task_count, 0);
534
+ const L = [];
535
+ L.push("<!-- AUTO-GENERATED by `task docs:rule-map` (rule-map CLI verb) — DO NOT EDIT MANUALLY -->");
536
+ L.push("<!-- Source of truth: content/ + tasks/ + Taskfile.yml -->");
537
+ L.push("<!-- Interactive explorer (gitignored): run `task docs:rule-map`, then open docs/rule-map/index.html -->");
538
+ L.push("");
539
+ L.push("# Directive Rule Map");
540
+ L.push("");
541
+ L.push("Maintainer-facing map of how Directive's rules are layered and grouped. This is a " +
542
+ "*derived view* — the source of truth is `content/`, `tasks/`, and the packs. Regenerate " +
543
+ "with `task docs:rule-map` after changing rules; do not hand-edit.");
544
+ L.push("");
545
+ L.push("## Overview");
546
+ L.push("");
547
+ L.push(`- **Rules:** ${g.length} groupings, ${totalDocs} documents`);
548
+ L.push(`- **Tasks:** ${tk.length} namespaces, ${totalTasks} tasks`);
549
+ if (packs.length) {
550
+ const compiled = packs.reduce((a, p) => a + (p.rule_count ?? 0), 0);
551
+ L.push(`- **Packs:** ${packs.length} source-of-truth packs (${compiled} compiled rules)`);
552
+ }
553
+ L.push("");
554
+ L.push("Three layers: **Rules** (lazy-loaded guidance under `content/`), **Tasks** (the Taskfile " +
555
+ "gates that enforce them), and the **Lifecycle** that ties them together.");
556
+ L.push("");
557
+ L.push("## Rule groupings");
558
+ L.push("");
559
+ const grows = g.map((x) => {
560
+ const mk = x.markers;
561
+ return [
562
+ x.name,
563
+ x.purpose || "—",
564
+ x.doc_count,
565
+ mk["MUST"] ?? 0,
566
+ mk["SHOULD"] ?? 0,
567
+ mk["MUST NOT"] ?? 0,
568
+ mk["SHOULD NOT"] ?? 0,
569
+ mk["MAY"] ?? 0,
570
+ ];
571
+ });
572
+ L.push(mdTable(["Grouping", "Purpose", "Docs", "MUST", "SHOULD", "MUST NOT", "SHOULD NOT", "MAY"], grows));
573
+ L.push("");
574
+ for (const x of g) {
575
+ L.push(`### ${x.name}`);
576
+ L.push("");
577
+ if (x.purpose) {
578
+ L.push(`_${x.purpose}_`);
579
+ L.push("");
580
+ }
581
+ for (const it of x.items) {
582
+ if (it.kind === "file") {
583
+ L.push(`- \`${it.name}\`${it.summary ? ` — ${it.summary}` : ""}`);
584
+ }
585
+ else {
586
+ L.push(`- **${it.name}/** (${it.count} files)${it.summary ? ` — ${it.summary}` : ""}`);
587
+ }
588
+ }
589
+ L.push("");
590
+ }
591
+ L.push("## Task namespaces");
592
+ L.push("");
593
+ L.push(mdTable(["Namespace", "Purpose", "Tasks"], tk.map((t) => [t.namespace, t.purpose || "—", t.task_count])));
594
+ L.push("");
595
+ const Lf = model.lifecycle;
596
+ L.push("## Lifecycle");
597
+ L.push("");
598
+ L.push(Lf.summary);
599
+ L.push("");
600
+ L.push("**Rule strength (prefer enforceable over remembered):** " +
601
+ Lf.rule_strength.map((r) => `${r.level} (${r.detail})`).join(" → "));
602
+ L.push("");
603
+ L.push("**Scope lifecycle:** " + Lf.scope_states.map((s) => `${s.state} [\`${s.via}\`]`).join(" → "));
604
+ L.push("");
605
+ L.push("**Quality gates:** " + Lf.gates.map((x) => `\`${x}\``).join(", "));
606
+ L.push("");
607
+ if (packs.length) {
608
+ L.push("## Packs (source of truth)");
609
+ L.push("");
610
+ const prows = packs.map((p) => {
611
+ const t = p.tiers ?? {};
612
+ return [
613
+ `\`${p.path}\``,
614
+ p.version || "—",
615
+ p.rule_count ?? "—",
616
+ t["MUST"] ?? 0,
617
+ t["SHOULD"] ?? 0,
618
+ t["MUST_NOT"] ?? 0,
619
+ ];
620
+ });
621
+ L.push(mdTable(["Pack", "Version", "Rules", "MUST", "SHOULD", "MUST_NOT"], prows));
622
+ L.push("");
623
+ }
624
+ return `${L.join("\n").replace(/\n+$/, "")}\n`;
625
+ }
626
+ // --------------------------------------------------------------- HTML render
627
+ function renderHtml(model) {
628
+ const token = "/*__DATA__*/ null";
629
+ if (!TEMPLATE.includes(token)) {
630
+ throw new Error(`rule-map template missing data injection token: ${token}`);
631
+ }
632
+ // Escape "<" so an embedded "</script>" or "<!--" in rule text can't terminate
633
+ // the inline <script>. Inject via a replacer FUNCTION so that "$" sequences in
634
+ // the JSON (shell/regex snippets contain $&, $1, $') are inserted verbatim
635
+ // rather than being interpreted by String.prototype.replace.
636
+ const data = JSON.stringify(model).replace(/</g, "\\u003c");
637
+ return TEMPLATE.replace(token, () => data);
638
+ }
639
+ // --------------------------------------------------------------- main
640
+ function resolveFrameworkRoot() {
641
+ const env = process.env.DEFT_ROOT;
642
+ if (env)
643
+ return env;
644
+ // dist/render/rule-map.js -> repo root is 4 levels up (packages/core/dist/render)
645
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
646
+ }
647
+ export function main(argv) {
648
+ if (argv.includes("--help") || argv.includes("-h")) {
649
+ process.stdout.write("usage: rule-map [--project-root <dir>] [--check]\n" +
650
+ " builds docs/RULE-MAP.md (committed) and docs/rule-map/index.html (gitignored)\n");
651
+ return 0;
652
+ }
653
+ let projectRoot;
654
+ let check = false;
655
+ for (let i = 0; i < argv.length; i += 1) {
656
+ const arg = argv[i];
657
+ if (arg === "--project-root") {
658
+ projectRoot = argv[i + 1];
659
+ i += 1;
660
+ }
661
+ else if (arg.startsWith("--project-root=")) {
662
+ projectRoot = arg.slice("--project-root=".length);
663
+ }
664
+ else if (arg === "--check") {
665
+ check = true;
666
+ }
667
+ }
668
+ const repo = resolve(projectRoot ?? resolveFrameworkRoot());
669
+ if (!existsSync(join(repo, "content"))) {
670
+ process.stderr.write(`error: ${repo} is not a Directive repo (no content/)\n`);
671
+ return 2;
672
+ }
673
+ const mdPath = join(repo, "docs", "RULE-MAP.md");
674
+ const htmlDir = join(repo, "docs", "rule-map");
675
+ const htmlPath = join(htmlDir, "index.html");
676
+ const model = buildModel(repo, false);
677
+ const md = renderMd(model);
678
+ if (check) {
679
+ const current = existsSync(mdPath) ? readFileSync(mdPath, "utf8") : "";
680
+ // Normalize CRLF so a Windows checkout (core.autocrlf=true) isn't reported STALE.
681
+ if (current.replace(/\r\n/g, "\n") !== md) {
682
+ process.stderr.write("STALE: docs/RULE-MAP.md is out of date — run `task docs:rule-map`\n");
683
+ return 1;
684
+ }
685
+ process.stdout.write("ok: docs/RULE-MAP.md is up to date\n");
686
+ return 0;
687
+ }
688
+ // Preflight both destinations before either write so a later HTML refusal
689
+ // cannot leave RULE-MAP.md updated while docs/rule-map/index.html stays stale.
690
+ assertWriteTargetSafe(repo, mdPath);
691
+ assertWriteTargetSafe(repo, htmlPath);
692
+ mkdirSync(dirname(mdPath), { recursive: true });
693
+ writeFileSync(mdPath, md, "utf8");
694
+ mkdirSync(htmlDir, { recursive: true });
695
+ const html = renderHtml(buildModel(repo, true));
696
+ writeFileSync(htmlPath, html, "utf8");
697
+ // Reuse the no-body model built above for the summary counts (no third scan).
698
+ process.stdout.write(`✓ docs/RULE-MAP.md (${md.split("\n").length} lines, committed)\n`);
699
+ process.stdout.write(`✓ docs/rule-map/index.html (${Math.floor(html.length / 1024)} KB, gitignored, self-contained)\n`);
700
+ process.stdout.write(` groupings=${model.groupings.length} tasks=${model.tasks.length} packs=${model.packs.length}\n`);
701
+ process.stdout.write(`\nOpen the interactive map:\n ${htmlPath}\n`);
702
+ return 0;
703
+ }
704
+ // run-as-main guard (also lets `tsx rule-map.ts` work for local verification)
705
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
706
+ process.exit(main(process.argv.slice(2)));
707
+ }
708
+ //# sourceMappingURL=rule-map.js.map