@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
@@ -17,7 +17,7 @@ export interface ValueFeedbackConfig {
17
17
  readonly sessionLine: boolean;
18
18
  readonly upstreamPrompt: boolean;
19
19
  }
20
- export type ValueFeedbackSource = "typed" | "org-auto" | "default" | "default-on-error";
20
+ export type ValueFeedbackSource = "typed" | "org-auto" | "install-force-on" | "default" | "default-on-error";
21
21
  export interface ValueFeedbackResolved extends ValueFeedbackConfig {
22
22
  readonly source: ValueFeedbackSource;
23
23
  readonly error: string | null;
@@ -25,6 +25,8 @@ export interface ValueFeedbackResolved extends ValueFeedbackConfig {
25
25
  export declare const VALUE_FEEDBACK_CAPABILITY_COST_DISCLOSURE: string;
26
26
  /** Validate a `plan.policy.valueFeedback` payload. */
27
27
  export declare function validateValueFeedback(value: unknown): string[];
28
+ /** Resolve a typed `valueFeedback` block without org-auto / install-force-on layers. */
29
+ export declare function resolveValueFeedbackFromTypedBlock(raw: unknown): ValueFeedbackResolved;
28
30
  export interface ResolveValueFeedbackOptions {
29
31
  /** Test seam for origin-org auto-enable resolution (#2376). */
30
32
  readonly autoEnable?: OrgAutoEnableOptions;
@@ -78,4 +80,19 @@ export interface EnableValueFeedbackResult {
78
80
  }
79
81
  /** Persist `valueFeedback.enabled=true` after capability-cost disclosure (#1709). */
80
82
  export declare function enableValueFeedback(projectRoot: string, options: EnableValueFeedbackOptions): EnableValueFeedbackResult;
83
+ export interface ClearValueFeedbackOptions {
84
+ readonly actor?: string;
85
+ readonly note?: string;
86
+ }
87
+ export interface ClearValueFeedbackResult {
88
+ readonly exitCode: 0 | 2;
89
+ readonly stdout: string;
90
+ readonly changed: boolean;
91
+ }
92
+ /**
93
+ * Remove the typed `valueFeedback` key so trusted-org resolution can return to
94
+ * org-auto (#2822). Does not delete the install-force-on marker — subsequent
95
+ * updates will not re-force against intentional opt-out.
96
+ */
97
+ export declare function clearValueFeedback(projectRoot: string, options?: ClearValueFeedbackOptions): ClearValueFeedbackResult;
81
98
  //# sourceMappingURL=value-feedback.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
3
+ import { valueFeedbackInstallForceOnSource } from "./org-force-on-migration.js";
3
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
4
5
  import { policyColonInvocation } from "./policy-invocation.js";
5
6
  import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
@@ -77,7 +78,8 @@ export function validateValueFeedback(value) {
77
78
  }
78
79
  return errors;
79
80
  }
80
- function resolveFromPolicyBlock(raw) {
81
+ /** Resolve a typed `valueFeedback` block without org-auto / install-force-on layers. */
82
+ export function resolveValueFeedbackFromTypedBlock(raw) {
81
83
  const errors = validateValueFeedback(raw);
82
84
  if (errors.length > 0) {
83
85
  return defaultResolved("default-on-error", errors[0] ?? "invalid valueFeedback block");
@@ -130,7 +132,13 @@ export function resolveValueFeedback(projectRoot, options = {}) {
130
132
  }
131
133
  return defaultResolved("default");
132
134
  }
133
- return resolveFromPolicyBlock(policyBlock.valueFeedback);
135
+ const raw = policyBlock.valueFeedback;
136
+ const installSource = valueFeedbackInstallForceOnSource(projectRoot, raw);
137
+ if (installSource !== null) {
138
+ const resolved = resolveValueFeedbackFromTypedBlock(raw);
139
+ return { ...resolved, source: installSource };
140
+ }
141
+ return resolveValueFeedbackFromTypedBlock(raw);
134
142
  }
135
143
  /** Master gate: when `enabled` is false, every downstream path is rejected. */
136
144
  export function isValueFeedbackPathAllowed(path, policy) {
@@ -196,8 +204,10 @@ export function inspectValueFeedback(data, projectRoot, options = {}) {
196
204
  }
197
205
  return fieldFromResolved(defaultResolved("default"));
198
206
  }
199
- const resolved = resolveFromPolicyBlock(policyBlock.valueFeedback);
200
- return fieldFromResolved(resolved);
207
+ const raw = policyBlock.valueFeedback;
208
+ const installSource = projectRoot !== undefined ? valueFeedbackInstallForceOnSource(projectRoot, raw) : null;
209
+ const resolved = resolveValueFeedbackFromTypedBlock(raw);
210
+ return fieldFromResolved(installSource !== null ? { ...resolved, source: installSource } : resolved);
201
211
  }
202
212
  /** Persist `valueFeedback.enabled=true` after capability-cost disclosure (#1709). */
203
213
  export function enableValueFeedback(projectRoot, options) {
@@ -259,7 +269,7 @@ export function enableValueFeedback(projectRoot, options) {
259
269
  sessionLine: readPersistedSubFlag("sessionLine"),
260
270
  upstreamPrompt: readPersistedSubFlag("upstreamPrompt"),
261
271
  };
262
- const previousNormalized = resolveFromPolicyBlock(previous);
272
+ const previousNormalized = resolveValueFeedbackFromTypedBlock(previous);
263
273
  const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
264
274
  previousNormalized.emitEvents !== nextBlock.emitEvents ||
265
275
  previousNormalized.sessionLine !== nextBlock.sessionLine ||
@@ -302,4 +312,67 @@ export function enableValueFeedback(projectRoot, options) {
302
312
  return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
303
313
  }
304
314
  }
315
+ /**
316
+ * Remove the typed `valueFeedback` key so trusted-org resolution can return to
317
+ * org-auto (#2822). Does not delete the install-force-on marker — subsequent
318
+ * updates will not re-force against intentional opt-out.
319
+ */
320
+ export function clearValueFeedback(projectRoot, options = {}) {
321
+ const path = projectDefinitionPath(projectRoot);
322
+ try {
323
+ const { changed } = projectDefinitionMutationLock(projectRoot, () => {
324
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
325
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
326
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
327
+ }
328
+ const data = parsed;
329
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
330
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
331
+ }
332
+ const plan = data.plan;
333
+ migrateLegacyPolicyKey(plan);
334
+ const existingPolicy = plan[PLAN_POLICY_KEY];
335
+ if (typeof existingPolicy !== "object" ||
336
+ existingPolicy === null ||
337
+ Array.isArray(existingPolicy)) {
338
+ return { changed: false };
339
+ }
340
+ const policyBlock = existingPolicy;
341
+ if (!("valueFeedback" in policyBlock)) {
342
+ return { changed: false };
343
+ }
344
+ const previous = policyBlock.valueFeedback;
345
+ delete policyBlock.valueFeedback;
346
+ atomicWriteProjectDefinition(path, data);
347
+ const actor = options.actor ?? policyColonInvocation("clear-value-feedback");
348
+ const note = options.note ?? "";
349
+ const parts = [
350
+ `actor=${actor}`,
351
+ "valueFeedback=cleared",
352
+ `previous=${JSON.stringify(previous ?? null)}`,
353
+ ];
354
+ if (note) {
355
+ parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
356
+ }
357
+ appendAuditLog(projectRoot, parts.join(" "));
358
+ return { changed: true };
359
+ });
360
+ const resolved = resolveValueFeedback(projectRoot);
361
+ const lines = [
362
+ `\u2713 ${FIELD_VALUE_FEEDBACK} typed key removed (resolution returns to org-auto/default).`,
363
+ changed
364
+ ? " audit: meta/policy-changes.log updated."
365
+ : " no-op: typed key was already absent.",
366
+ formatValueFeedbackStatusLine(resolved),
367
+ ];
368
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
369
+ }
370
+ catch (err) {
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ if (message.includes("PROJECT-DEFINITION not found")) {
373
+ return { exitCode: 2, stdout: `\u274c ${message}\n`, changed: false };
374
+ }
375
+ return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
376
+ }
377
+ }
305
378
  //# sourceMappingURL=value-feedback.js.map
@@ -14,7 +14,7 @@ import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, relative } from "node:path";
16
16
  import { isTriageCacheEmpty, maybeAutoPopulateEmptyCache } from "../cache/empty-populate.js";
17
- import { probeCacheDrift } from "../cache/fetch.js";
17
+ import { probeCacheDrift, readOpenInventoryStamp, } from "../cache/fetch.js";
18
18
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
19
19
  import { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
20
20
  export { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
@@ -423,6 +423,17 @@ function evaluateWithContext(projectRoot, options, ctx) {
423
423
  minMetaPath = p;
424
424
  }
425
425
  }
426
+ // #2826: empty open inventory with a successful fetch stamp is fresh — not
427
+ // the never-fetched case that yields minFetchedAt=null / Infinity age.
428
+ if (minFetchedAt === null && rawCandidatePaths.length === 0 && resolvedRepo !== null) {
429
+ const stamp = readOpenInventoryStamp(cacheRoot, source, resolvedRepo);
430
+ if (stamp !== null) {
431
+ const parsed = new Date(stamp.fetched_at);
432
+ if (!Number.isNaN(parsed.getTime())) {
433
+ minFetchedAt = parsed;
434
+ }
435
+ }
436
+ }
426
437
  const now = nowFn();
427
438
  const ageMs = minFetchedAt !== null ? now.getTime() - minFetchedAt.getTime() : Infinity;
428
439
  const ageH = ageMs / (1000 * 3600);
@@ -0,0 +1,13 @@
1
+ import { type HeadlessDetectionOptions } from "./headless.js";
2
+ /** D17 first-time consent ask (#2693 / #2822). */
3
+ export declare const PRODUCT_SIGNAL_CONSENT_PROMPT: string;
4
+ export interface ProductSignalConsentEligibilityOptions extends HeadlessDetectionOptions {
5
+ readonly projectRoot: string;
6
+ }
7
+ /** True when an interactive session should surface the D17 consent ask (#2822 AC). */
8
+ export declare function isProductSignalConsentEligible(options: ProductSignalConsentEligibilityOptions): boolean;
9
+ /** Format the D17 consent ask for session start / status surfaces. */
10
+ export declare function formatProductSignalConsentPrompt(): string;
11
+ /** Emit the D17 ask when eligible; headless callers get an empty string (#2693 D16). */
12
+ export declare function maybeFormatProductSignalConsentPrompt(options: ProductSignalConsentEligibilityOptions): string;
13
+ //# sourceMappingURL=consent-prompt.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { resolveProductSignal } from "../policy/product-signal.js";
2
+ import { isProductSignalConsented } from "./consent.js";
3
+ import { isHeadlessSession } from "./headless.js";
4
+ /** D17 first-time consent ask (#2693 / #2822). */
5
+ export const PRODUCT_SIGNAL_CONSENT_PROMPT = "May we collect usage metrics and related session signal from this install to improve Directive? " +
6
+ "This can include a short check-in plus minimized local summaries (value/health and related ledgers). " +
7
+ "Nothing is sent while this path is off or without your consent.\n\n" +
8
+ "If you are unsure, please check with your company before saying no — they are a Directive partner " +
9
+ "and may already expect this signal to be shared.\n\n" +
10
+ "Reply **yes** to consent (`task product-signal:consent -- --grant`), or **no** to decline.";
11
+ /** True when an interactive session should surface the D17 consent ask (#2822 AC). */
12
+ export function isProductSignalConsentEligible(options) {
13
+ if (isHeadlessSession(options)) {
14
+ return false;
15
+ }
16
+ const policy = resolveProductSignal(options.projectRoot);
17
+ if (!policy.enabled) {
18
+ return false;
19
+ }
20
+ return !isProductSignalConsented({ env: options.env });
21
+ }
22
+ /** Format the D17 consent ask for session start / status surfaces. */
23
+ export function formatProductSignalConsentPrompt() {
24
+ return `[deft product-signal] First-time consent eligible:\n${PRODUCT_SIGNAL_CONSENT_PROMPT}\n`;
25
+ }
26
+ /** Emit the D17 ask when eligible; headless callers get an empty string (#2693 D16). */
27
+ export function maybeFormatProductSignalConsentPrompt(options) {
28
+ return isProductSignalConsentEligible(options) ? formatProductSignalConsentPrompt() : "";
29
+ }
30
+ //# sourceMappingURL=consent-prompt.js.map
@@ -1,9 +1,11 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
4
  import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
4
5
  import { resolveProjectRoot } from "../scope/project-context.js";
5
6
  import { resolveActorName } from "./actor-name.js";
6
7
  import { authorizeProductSignalSink, grantProductSignalConsent, readProductSignalConsent, revokeProductSignalConsent, } from "./consent.js";
8
+ import { maybeFormatProductSignalConsentPrompt } from "./consent-prompt.js";
7
9
  import { evaluateProductSignalGates } from "./gates.js";
8
10
  import { GitHubPrivateSinkAdapter } from "./github-private-sink-adapter.js";
9
11
  import { collectInstallContext } from "./install-context.js";
@@ -13,6 +15,7 @@ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
13
15
  export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
14
16
  function recordLastSubmit(projectRoot, outcome, url) {
15
17
  const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
18
+ assertWriteTargetSafe(projectRoot, path);
16
19
  try {
17
20
  mkdirSync(join(path, ".."), { recursive: true });
18
21
  appendFileSync(path, `${JSON.stringify({
@@ -127,7 +130,21 @@ export async function submitProductSignal(options) {
127
130
  const adapter = new GitHubPrivateSinkAdapter({ sinkRepo: policy.sinkRepo });
128
131
  const result = await adapter.submit(payload, { gapText: options.gapText });
129
132
  if (result.outcome === "submitted") {
130
- recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
133
+ try {
134
+ recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
135
+ }
136
+ catch (err) {
137
+ if (err instanceof ProjectionContainmentError) {
138
+ return {
139
+ outcome: "error-config",
140
+ exitCode: 2,
141
+ message: `${err.message}\n`,
142
+ issueUrl: result.issueUrl,
143
+ payload,
144
+ };
145
+ }
146
+ throw err;
147
+ }
131
148
  }
132
149
  const urlLine = result.issueUrl ? ` url=${result.issueUrl}` : "";
133
150
  return {
@@ -150,6 +167,10 @@ export function runProductSignalStatus(projectRoot) {
150
167
  `[deft product-signal] consented=${String(consent !== null)} configuredSink=${configuredSink} consentedSink=${sinkAuth.consentedSink ?? "none"} sinksMatch=${String(sinkAuth.sinksMatch)}`,
151
168
  last ? `[deft product-signal] ${last}` : "[deft product-signal] last submit: none",
152
169
  ];
170
+ const consentPrompt = maybeFormatProductSignalConsentPrompt({ projectRoot: root });
171
+ if (consentPrompt.length > 0) {
172
+ lines.push(consentPrompt.trimEnd());
173
+ }
153
174
  return { exitCode: 0, text: `${lines.join("\n")}\n` };
154
175
  }
155
176
  export function runProductSignalEnable(projectRoot, confirm) {
@@ -25,7 +25,7 @@ export declare function formatFrameworkCommand(args: readonly string[], options?
25
25
  surface?: string;
26
26
  taskPrefix?: string | null;
27
27
  }): string;
28
- export declare function cmdCoreValidate(argv: readonly string[]): number;
28
+ export declare function cmdCoreValidate(argv: readonly string[], root?: string): number;
29
29
  export declare function cmdCoreLint(_argv: readonly string[]): number;
30
30
  export declare function cmdCoreTest(_argv: readonly string[]): number;
31
31
  export declare function resolveFrameworkRoot(): string;
@@ -154,7 +154,7 @@ export function formatFrameworkCommand(args, options = {}) {
154
154
  }
155
155
  return [surface, ...parts].join(" ");
156
156
  }
157
- const EXCLUDE_PARTS = new Set([".git", "backup"]);
157
+ const EXCLUDE_PARTS = new Set([".git", "backup", "node_modules", ".deft-scratch"]);
158
158
  function collectMarkdownFiles(root) {
159
159
  const out = [];
160
160
  const walk = (dir, parts) => {
@@ -189,12 +189,12 @@ function collectMarkdownFiles(root) {
189
189
  walk(root, []);
190
190
  return out.sort();
191
191
  }
192
- export function cmdCoreValidate(argv) {
192
+ export function cmdCoreValidate(argv, root = ".") {
193
193
  if (argv.length > 0) {
194
194
  process.stderr.write(`error: core:validate does not accept arguments: ${argv.join(" ")}\n`);
195
195
  return 2;
196
196
  }
197
- const files = collectMarkdownFiles(".");
197
+ const files = collectMarkdownFiles(root);
198
198
  for (const path of files)
199
199
  process.stdout.write(`✓ ${path}\n`);
200
200
  process.stdout.write(`✓ All ${files.length} markdown files validated\n`);
@@ -271,7 +271,7 @@ function runBuildDistArgv(argv) {
271
271
  return result.status ?? 1;
272
272
  }
273
273
  const TS_INLINE = {
274
- "framework_commands:_cmd_core_validate": (argv) => cmdCoreValidate(argv),
274
+ "framework_commands:_cmd_core_validate": (argv, cwd) => cmdCoreValidate(argv, cwd),
275
275
  "doctor:cmd_doctor": (argv) => cmdDoctor(argv),
276
276
  "build_dist:main": (argv) => runBuildDistArgv(argv),
277
277
  };
@@ -349,7 +349,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
349
349
  };
350
350
  let code;
351
351
  try {
352
- code = inline(argv);
352
+ code = inline(argv, cwd);
353
353
  }
354
354
  finally {
355
355
  process.stdout.write = prevOut;
@@ -357,7 +357,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
357
357
  }
358
358
  return { code, stdout: chunks.out, stderr: chunks.err };
359
359
  }
360
- return { code: inline(argv), stdout: "", stderr: "" };
360
+ return { code: inline(argv, cwd), stdout: "", stderr: "" };
361
361
  }
362
362
  const verb = ENTRYPOINT_VERB[entrypoint];
363
363
  if (verb) {
@@ -8,6 +8,7 @@ export * as projectRender from "./project-render.js";
8
8
  export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
9
9
  export * as roadmapRender from "./roadmap-render.js";
10
10
  export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
11
+ export { main as ruleMapMain } from "./rule-map.js";
11
12
  export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
12
13
  export * as specRender from "./spec-render.js";
13
14
  export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
@@ -8,6 +8,7 @@ export * as projectRender from "./project-render.js";
8
8
  export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
9
9
  export * as roadmapRender from "./roadmap-render.js";
10
10
  export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
11
+ export { main as ruleMapMain } from "./rule-map.js";
11
12
  export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
12
13
  export * as specRender from "./spec-render.js";
13
14
  export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
@@ -3,7 +3,11 @@ export declare function renderRoadmapToBuffer(pendingDir: string, completedDir?:
3
3
  /** @deprecated Prefer ``renderRoadmapToBuffer`` — kept for existing imports and parity harnesses. */
4
4
  export declare function generateRoadmapContent(pendingDir: string, completedDir?: string): string;
5
5
  export type RenderRoadmapResult = readonly [boolean, string];
6
- export declare function renderRoadmap(pendingDir: string, outPath: string, completedDir?: string): RenderRoadmapResult;
6
+ export type RenderRoadmapOptions = {
7
+ completedDir?: string;
8
+ projectRoot?: string;
9
+ };
10
+ export declare function renderRoadmap(pendingDir: string, outPath: string, completedDirOrOptions?: string | RenderRoadmapOptions): RenderRoadmapResult;
7
11
  export declare function checkDrift(pendingDir: string, roadmapPath: string, completedDir?: string): RenderRoadmapResult;
8
12
  /** CLI entry (mirrors ``scripts/roadmap_render.main``). */
9
13
  export declare function main(argv: readonly string[]): number;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
5
  import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER } from "./constants.js";
5
6
  import { phaseSortKey } from "./text-utils.js";
@@ -351,9 +352,22 @@ export function renderRoadmapToBuffer(pendingDir, completedDir) {
351
352
  export function generateRoadmapContent(pendingDir, completedDir) {
352
353
  return renderRoadmapToBuffer(pendingDir, completedDir);
353
354
  }
354
- export function renderRoadmap(pendingDir, outPath, completedDir) {
355
+ export function renderRoadmap(pendingDir, outPath, completedDirOrOptions) {
356
+ let completedDir;
357
+ let projectRoot;
358
+ if (typeof completedDirOrOptions === "string") {
359
+ completedDir = completedDirOrOptions;
360
+ }
361
+ else if (completedDirOrOptions !== undefined) {
362
+ completedDir = completedDirOrOptions.completedDir;
363
+ projectRoot = completedDirOrOptions.projectRoot;
364
+ }
355
365
  try {
356
366
  const content = renderRoadmapToBuffer(pendingDir, completedDir);
367
+ // Trust boundary is the project root — never dirname(outPath), which follows a
368
+ // diverted parent symlink and would make containment pass outside the checkout.
369
+ const projectDir = projectRoot !== undefined ? resolve(projectRoot) : resolve(pendingDir, "..", "..");
370
+ assertWriteTargetSafe(projectDir, resolve(outPath));
357
371
  writeFileSync(outPath, content, "utf8");
358
372
  return [true, `✓ Rendered ROADMAP.md to ${outPath}`];
359
373
  }
@@ -415,7 +429,11 @@ export function main(argv) {
415
429
  process.stdout.write(`${msg}\n`);
416
430
  return ok ? 0 : 1;
417
431
  }
418
- const [ok, msg] = renderRoadmap(pendingDir, outPath);
432
+ // When --project-root is set, use it; otherwise renderRoadmap derives from pendingDir
433
+ // (…/xbrief|vbrief/pending → project root). Never use dirname(outPath).
434
+ const [ok, msg] = renderRoadmap(pendingDir, outPath, {
435
+ projectRoot: projectRoot !== undefined ? resolve(projectRoot) : undefined,
436
+ });
419
437
  process.stdout.write(`${msg}\n`);
420
438
  return ok ? 0 : 1;
421
439
  }
@@ -0,0 +1,2 @@
1
+ export declare const TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Directive \u2014 Rule Map</title>\n<style>\n :root{\n --bg:#0f1115; --panel:#171a21; --panel2:#1d222b; --line:#2a303b;\n --ink:#e7ebf0; --dim:#96a0ad; --dim2:#6b7482; --accent:#6ea8fe; --accent2:#8be0c0;\n --must:#ff6b6b; --mustnot:#c0392b; --should:#f0b429; --shouldnot:#9a7b12; --may:#7f8a99;\n --radius:12px;\n }\n *{box-sizing:border-box}\n html,body{margin:0;height:100%}\n body{background:var(--bg);color:var(--ink);\n font:15px/1.5 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif}\n code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}\n a{color:var(--accent);text-decoration:none}\n .wrap{max-width:1120px;margin:0 auto;padding:22px 24px 80px}\n header.top{display:flex;align-items:center;gap:18px;flex-wrap:wrap;\n border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:20px}\n .brand{font-size:20px;font-weight:700;letter-spacing:.2px}\n .brand small{display:block;font-size:12px;color:var(--dim);font-weight:400;margin-top:2px}\n nav.spines{display:flex;gap:6px;margin-left:auto}\n nav.spines button{background:var(--panel);color:var(--dim);border:1px solid var(--line);\n padding:7px 14px;border-radius:999px;cursor:pointer;font-size:13px}\n nav.spines button.active{background:var(--accent);color:#0b0d10;border-color:var(--accent);font-weight:600}\n .search{width:100%;margin:0 0 18px}\n .search input{width:100%;background:var(--panel);border:1px solid var(--line);color:var(--ink);\n padding:11px 14px;border-radius:10px;font-size:14px}\n .search input::placeholder{color:var(--dim2)}\n .crumbs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--dim);font-size:13px;margin-bottom:16px}\n .crumbs a{cursor:pointer}\n .crumbs .sep{color:var(--dim2)}\n .crumbs .here{color:var(--ink)}\n .lead{color:var(--dim);margin:-4px 0 20px;max-width:760px}\n .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}\n .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);\n padding:15px 16px;cursor:pointer;transition:.12s;display:flex;flex-direction:column;gap:7px;min-height:96px}\n .card:hover{border-color:var(--accent);background:var(--panel2);transform:translateY(-1px)}\n .card .name{font-weight:600;font-size:15px;display:flex;align-items:center;gap:8px}\n .card .name .ic{color:var(--dim2);font-size:13px}\n .card .desc{color:var(--dim);font-size:13px;line-height:1.45}\n .card .meta{margin-top:auto;display:flex;align-items:center;gap:10px;color:var(--dim2);font-size:12px}\n .rows{display:flex;flex-direction:column;gap:8px}\n .row{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px;\n cursor:pointer;transition:.12s;display:flex;align-items:center;gap:12px}\n .row:hover{border-color:var(--accent);background:var(--panel2)}\n .row .rname{font-weight:600;min-width:0}\n .row .rname .sub{font-weight:400;color:var(--dim);font-size:13px;display:block;margin-top:2px;\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row .rt{margin-left:auto;display:flex;gap:5px;flex-shrink:0}\n .row .chev{color:var(--dim2);flex-shrink:0}\n .tier{font-size:11px;padding:2px 7px;border-radius:999px;font-weight:600;white-space:nowrap;\n color:#0b0d10;font-family:ui-monospace,monospace}\n .t-MUST{background:var(--must)} .t-MUST-NOT{background:var(--mustnot);color:#fff}\n .t-SHOULD{background:var(--should)} .t-SHOULD-NOT{background:var(--shouldnot);color:#fff}\n .t-MAY{background:var(--may);color:#fff}\n .bar{display:flex;height:6px;border-radius:999px;overflow:hidden;background:var(--panel2);flex:1;max-width:120px}\n .bar span{display:block;height:100%}\n .detail{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:22px 24px}\n .detail h2{margin:0 0 4px;font-size:22px}\n .detail .kind{color:var(--dim2);font-size:12px;text-transform:uppercase;letter-spacing:.08em}\n .detail .summary{color:var(--dim);margin:12px 0 18px;max-width:720px}\n .sectionlist{display:flex;flex-wrap:wrap;gap:7px;margin:6px 0 4px}\n .sectionlist .s{background:var(--panel2);border:1px solid var(--line);border-radius:7px;\n padding:5px 10px;font-size:12.5px;color:var(--ink)}\n .srcbox{margin-top:20px;border-top:1px solid var(--line);padding-top:16px}\n .srcbox .path{font-size:12.5px;color:var(--dim);word-break:break-all;margin-bottom:10px}\n .btns{display:flex;gap:8px;flex-wrap:wrap}\n button.act{background:var(--accent);color:#0b0d10;border:none;padding:8px 14px;border-radius:8px;\n cursor:pointer;font-size:13px;font-weight:600}\n button.act.ghost{background:transparent;color:var(--accent);border:1px solid var(--line)}\n button.act:hover{filter:brightness(1.08)}\n h3.grouphdr{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim2);margin:26px 0 10px}\n .legend{display:flex;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dim);margin:2px 0 18px}\n .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--panel2);\n border:1px solid var(--line);color:var(--ink);padding:11px 18px;border-radius:10px;font-size:13px;\n opacity:0;transition:.2s;pointer-events:none;box-shadow:0 8px 30px rgba(0,0,0,.4);z-index:50}\n .toast.show{opacity:1}\n .toast.err{border-color:var(--must)}\n .flow{display:flex;flex-wrap:wrap;align-items:stretch;gap:0}\n .flow .node{background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:10px 14px;text-align:center;min-width:110px}\n .flow .node .st{font-weight:600}\n .flow .node .via{font-size:11px;color:var(--dim2);margin-top:3px;font-family:ui-monospace,monospace}\n .flow .arw{align-self:center;color:var(--dim2);padding:0 8px;font-size:18px}\n .ladder{display:flex;flex-direction:column;gap:8px;margin:6px 0 20px}\n .ladder .lv{display:flex;gap:12px;align-items:baseline;background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:11px 14px}\n .ladder .lv b{min-width:190px}\n .ladder .lv .d{color:var(--dim);font-size:13px}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dim);\n background:var(--panel2);border:1px solid var(--line);padding:4px 9px;border-radius:999px;margin:0 6px 6px 0}\n .muted{color:var(--dim2);font-size:12px}\n .empty{color:var(--dim);padding:30px;text-align:center}\n .foot{margin-top:34px;color:var(--dim2);font-size:12px;border-top:1px solid var(--line);padding-top:14px}\n /* source viewer */\n .src{margin-top:16px;background:#0b0d12;border:1px solid var(--line);border-radius:10px;padding:16px 18px;\n max-height:60vh;overflow:auto}\n .src h1,.src h2,.src h3{color:var(--ink);margin:14px 0 6px;line-height:1.3}\n .src h1{font-size:19px} .src h2{font-size:16px} .src h3{font-size:14px}\n .src p{margin:8px 0;color:#c7cfda}\n .src ul{margin:8px 0;padding-left:22px;color:#c7cfda}\n .src li{margin:3px 0}\n .src code{background:#161b24;border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:12.5px;color:var(--accent2)}\n .src pre{background:#161b24;border:1px solid var(--line);border-radius:8px;padding:12px 14px;overflow:auto}\n .src pre code{background:none;border:none;padding:0;color:#c7cfda}\n .src blockquote{border-left:3px solid var(--line);margin:8px 0;padding:2px 12px;color:var(--dim)}\n .src .trunc{color:var(--dim2);font-size:12px;margin-top:10px}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header class=\"top\">\n <div class=\"brand\">Directive &mdash; Rule Map<small id=\"sub\">maintainer map</small></div>\n <nav class=\"spines\">\n <button data-spine=\"rules\">Rules</button>\n <button data-spine=\"tasks\">Tasks</button>\n <button data-spine=\"lifecycle\">Lifecycle</button>\n </nav>\n </header>\n\n <div class=\"search\"><input id=\"q\" placeholder=\"Search groupings, rules, tasks\u2026 (Esc to clear)\"></div>\n <div class=\"crumbs\" id=\"crumbs\"></div>\n <div id=\"view\"></div>\n <div class=\"foot\" id=\"foot\"></div>\n</div>\n<div class=\"toast\" id=\"toast\"></div>\n\n<script>window.DIRECTIVE_DATA = /*__DATA__*/ null;</script>\n<script>\nlet DATA=window.DIRECTIVE_DATA, spine=\"rules\", stack=[];\nconst el=(t,c,txt)=>{const e=document.createElement(t);if(c)e.className=c;if(txt!=null)e.textContent=txt;return e;};\nconst tierClass=t=>\"t-\"+t.replace(/\\s|_/g,\"-\");\nconst TIER_ORDER=[\"MUST\",\"MUST NOT\",\"SHOULD\",\"SHOULD NOT\",\"MAY\"];\nconst TIER_COLOR={\"MUST\":\"var(--must)\",\"MUST NOT\":\"var(--mustnot)\",\"SHOULD\":\"var(--should)\",\"SHOULD NOT\":\"var(--shouldnot)\",\"MAY\":\"var(--may)\"};\nconst MARK2TIER={\"!\":\"MUST\",\"\u2297\":\"MUST NOT\",\"~\":\"SHOULD\",\"\u2249\":\"SHOULD NOT\",\"?\":\"MAY\"};\n\nfunction toast(msg,err){const t=document.getElementById('toast');t.textContent=msg;\n t.className='toast show'+(err?' err':'');setTimeout(()=>t.className='toast',1800);}\nfunction copyPath(p){\n const ok=()=>toast(\"Path copied\");\n const fail=()=>toast(\"Copy failed\",true);\n const fallback=()=>{\n try{\n const ta=document.createElement(\"textarea\");\n ta.value=p;ta.setAttribute(\"readonly\",\"\");\n ta.style.position=\"fixed\";ta.style.left=\"-9999px\";\n document.body.appendChild(ta);ta.select();\n const done=document.execCommand(\"copy\");\n document.body.removeChild(ta);\n done?ok():fail();\n }catch(_){fail();}\n };\n if(navigator.clipboard&&typeof navigator.clipboard.writeText===\"function\"){\n navigator.clipboard.writeText(p).then(ok,fallback);\n }else fallback();\n}\n\n/* tiny zero-dependency markdown renderer (headings, lists, code, quote, inline) */\nfunction esc(s){return s.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\");}\nfunction inlineMd(s){\n s=esc(s);\n s=s.replace(/`([^`]+)`/g,'<code>$1</code>');\n s=s.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n s=s.replace(/(^|[^*])\\*([^*]+)\\*/g,'$1<em>$2</em>');\n s=s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,function(_,t,u){return /^(?:https?:|[/#.])/i.test(u)?'<a href=\"'+u+'\">'+t+'</a>':t;});\n return s;\n}\nfunction mdToHtml(md){\n const lines=(md||\"\").split(\"\\n\");\n let out=[], i=0, inCode=false, code=[], list=false;\n const closeList=()=>{if(list){out.push(\"</ul>\");list=false;}};\n while(i<lines.length){\n let ln=lines[i];\n if(/^```/.test(ln)){\n if(!inCode){inCode=true;code=[];}\n else{inCode=false;out.push(\"<pre><code>\"+esc(code.join(\"\\n\"))+\"</code></pre>\");}\n i++;continue;\n }\n if(inCode){code.push(ln);i++;continue;}\n let m;\n if((m=ln.match(/^(#{1,3})\\s+(.*)$/))){closeList();out.push(`<h${m[1].length}>`+inlineMd(m[2])+`</h${m[1].length}>`);}\n else if(/^\\s*[-*]\\s+/.test(ln)){if(!list){out.push(\"<ul>\");list=true;}out.push(\"<li>\"+inlineMd(ln.replace(/^\\s*[-*]\\s+/,\"\"))+\"</li>\");}\n else if(/^\\s*>\\s?/.test(ln)){closeList();out.push(\"<blockquote>\"+inlineMd(ln.replace(/^\\s*>\\s?/,\"\"))+\"</blockquote>\");}\n else if(ln.trim()===\"\"){closeList();}\n else{closeList();out.push(\"<p>\"+inlineMd(ln)+\"</p>\");}\n i++;\n }\n closeList();\n if(inCode)out.push(\"<pre><code>\"+esc(code.join(\"\\n\"))+\"</code></pre>\");\n return out.join(\"\\n\");\n}\n\nfunction markerBar(markers){\n const total=Object.values(markers||{}).reduce((a,b)=>a+b,0);\n const bar=el('div','bar'); if(!total) return bar;\n TIER_ORDER.forEach(t=>{const v=markers[t]||0;if(!v)return;\n const s=el('span');s.style.width=(v/total*100)+'%';s.style.background=TIER_COLOR[t];s.title=t+\": \"+v;bar.appendChild(s);});\n return bar;\n}\nfunction markersFromMarks(m){const o={};for(const k in (m||{})){const t=MARK2TIER[k];if(t)o[t]=(o[t]||0)+m[k];}return o;}\nfunction totalMarks(m){return Object.values(m||{}).reduce((a,b)=>a+b,0);}\n\nfunction go(node){stack.push(node);render();}\nfunction reset(s){spine=s;stack=[];render();}\nfunction crumbTo(i){stack=stack.slice(0,i);render();}\nfunction setSpine(s){document.querySelectorAll('nav.spines button').forEach(b=>b.classList.toggle('active',b.dataset.spine===s));}\n\nfunction render(){\n setSpine(spine);\n const view=document.getElementById('view');view.innerHTML='';\n renderCrumbs();\n if(document.getElementById('q').value.trim()){renderSearch(view);return;}\n if(stack.length===0){ ({rules:overviewRules,tasks:overviewTasks,lifecycle:renderLifecycle})[spine](view); return; }\n stack[stack.length-1].render(view);\n}\nfunction renderCrumbs(){\n const c=document.getElementById('crumbs');c.innerHTML='';\n const root=el('a','',spine[0].toUpperCase()+spine.slice(1));root.onclick=()=>{stack=[];render();};c.appendChild(root);\n stack.forEach((n,i)=>{c.appendChild(el('span','sep','\u203A'));\n if(i<stack.length-1){const a=el('a','',n.label);a.onclick=()=>crumbTo(i+1);c.appendChild(a);}\n else c.appendChild(el('span','here',n.label));});\n}\nfunction intro(t){return el('div','lead',t);}\nfunction ghdr(t){return el('h3','grouphdr',t);}\nfunction pathBar(p){const box=el('div','srcbox');box.appendChild(el('div','path',p));\n const b=el('button','act ghost','Copy path');b.onclick=e=>{e.stopPropagation();copyPath(p);};\n const btns=el('div','btns');btns.appendChild(b);box.appendChild(btns);return box;}\n\nfunction overviewRules(view){\n document.getElementById('foot').textContent=\n `${DATA.groupings.length} rule groupings \u00B7 ${DATA.groupings.reduce((a,g)=>a+g.doc_count,0)} docs \u00B7 derived from content/`;\n view.appendChild(intro(\"Major rule groupings \u2014 lazy-loaded bodies of guidance the agent pulls in only when the work needs it. Click to drill in.\"));\n const grid=el('div','grid');\n DATA.groupings.forEach(g=>{\n const card=el('div','card');\n card.appendChild(el('div','name',g.name));\n card.appendChild(el('div','desc',g.purpose||''));\n const meta=el('div','meta');meta.appendChild(el('span','',g.doc_count+' docs'));meta.appendChild(markerBar(g.markers));card.appendChild(meta);\n card.onclick=()=>go({label:g.name,render:v=>groupView(v,g)});\n grid.appendChild(card);\n });\n view.appendChild(grid);\n}\nfunction groupView(view,g){\n view.appendChild(intro(g.purpose||''));\n const files=g.items.filter(i=>i.kind==='file'), dirs=g.items.filter(i=>i.kind==='dir');\n if(files.length){view.appendChild(ghdr(\"Documents\"));view.appendChild(rowsOf(files));}\n if(dirs.length){view.appendChild(ghdr(\"Topics\"));\n const grid=el('div','grid');\n dirs.forEach(d=>{const card=el('div','card');card.appendChild(el('div','name',d.title||d.name));\n if(d.summary)card.appendChild(el('div','desc',d.summary));\n const meta=el('div','meta');meta.appendChild(el('span','',d.count+' files'));card.appendChild(meta);\n card.onclick=()=>go({label:d.name,render:v=>dirView(v,d)});grid.appendChild(card);});\n view.appendChild(grid);\n }\n}\nfunction dirView(view,d){\n view.appendChild(intro(d.summary||''));\n if(d.readme){view.appendChild(ghdr(\"Overview\"));view.appendChild(rowsOf([d.readme]));}\n view.appendChild(ghdr(\"Files\"));view.appendChild(rowsOf(d.files));\n}\nfunction rowsOf(files){\n const box=el('div','rows');\n files.forEach(f=>{\n const row=el('div','row');\n const nm=el('div','rname');nm.appendChild(document.createTextNode(f.title||f.name));\n if(f.summary)nm.appendChild(el('span','sub',f.summary));\n row.appendChild(nm);\n const rt=el('div','rt');const mk=markersFromMarks(f.markers);\n TIER_ORDER.forEach(t=>{if(mk[t]){const b=el('span','tier '+tierClass(t),mk[t]+\" \"+t.replace(\" NOT\",\"\u0338\"));b.title=mk[t]+\" \"+t;rt.appendChild(b);}});\n row.appendChild(rt);row.appendChild(el('span','chev','\u203A'));\n row.onclick=()=>go({label:f.name,render:v=>docDetail(v,f)});\n box.appendChild(row);\n });\n return box;\n}\nfunction docDetail(view,f){\n const d=el('div','detail');\n d.appendChild(el('div','kind',(f.generated?'generated view \u00B7 ':'')+(f.lines?f.lines+' lines':'document')));\n d.appendChild(el('h2',null,f.title||f.name));\n if(f.summary)d.appendChild(el('div','summary',f.summary));\n const mk=markersFromMarks(f.markers);\n if(totalMarks(f.markers)){const p=el('div');p.style.margin='0 0 14px';\n TIER_ORDER.forEach(t=>{if(mk[t]){const b=el('span','tier '+tierClass(t));b.textContent=mk[t]+\" \"+t;b.style.marginRight='6px';p.appendChild(b);}});\n d.appendChild(p);}\n if(f.sections&&f.sections.length){d.appendChild(el('div','muted','Sections'));\n const sl=el('div','sectionlist');f.sections.forEach(s=>sl.appendChild(el('div','s',s)));d.appendChild(sl);}\n if(f.generated)d.appendChild(Object.assign(el('div','muted'),{textContent:'\u26A0 Auto-generated from a pack \u2014 edit the source pack, not this file.',style:'margin-top:12px'}));\n\n const box=el('div','srcbox');\n box.appendChild(el('div','path',f.path));\n const btns=el('div','btns');\n const srcPane=el('div','src');srcPane.style.display='none';\n let shown=false;\n const view_=el('button','act','View source');\n view_.onclick=e=>{e.stopPropagation();shown=!shown;\n if(shown&&!srcPane.dataset.rendered){srcPane.innerHTML=mdToHtml(f.body||'(source not captured)');\n if(f.truncated)srcPane.appendChild(Object.assign(el('div','trunc'),{textContent:'\u2026 truncated (large file); open the file directly for the rest.'}));\n srcPane.dataset.rendered='1';}\n srcPane.style.display=shown?'block':'none';view_.textContent=shown?'Hide source':'View source';};\n const copy=el('button','act ghost','Copy path');copy.onclick=e=>{e.stopPropagation();copyPath(f.path);};\n btns.appendChild(view_);btns.appendChild(copy);box.appendChild(btns);box.appendChild(srcPane);\n d.appendChild(box);\n view.appendChild(d);\n}\n\nfunction overviewTasks(view){\n document.getElementById('foot').textContent=\n `${DATA.tasks.length} task namespaces \u00B7 ${DATA.tasks.reduce((a,t)=>a+t.task_count,0)} tasks \u00B7 run: task <namespace>:<name>`;\n view.appendChild(intro(\"The executable layer. Rules are enforced by Taskfile targets \u2014 deterministic gates the agent and CI run. Each card is a namespace; drill in for its commands.\"));\n const grid=el('div','grid');\n DATA.tasks.forEach(t=>{const card=el('div','card');\n const nm=el('div','name');nm.appendChild(document.createTextNode(t.namespace));\n if(t.unlisted)nm.appendChild(Object.assign(el('span','ic'),{textContent:'unlisted'}));\n card.appendChild(nm);card.appendChild(el('div','desc',t.purpose||''));\n const meta=el('div','meta');meta.appendChild(el('span','',t.task_count+' tasks'));card.appendChild(meta);\n card.onclick=()=>go({label:t.namespace,render:v=>taskDetail(v,t)});grid.appendChild(card);});\n view.appendChild(grid);\n}\nfunction taskDetail(view,t){\n view.appendChild(intro(t.purpose||''));\n view.appendChild(ghdr(\"Commands\"));\n const box=el('div','rows');\n t.tasks.forEach(tk=>{const row=el('div','row');row.style.cursor='default';\n const nm=el('div','rname');nm.appendChild(Object.assign(el('code'),{textContent:'task '+t.namespace+':'+tk.name}));\n if(tk.desc)nm.appendChild(el('span','sub',tk.desc));row.appendChild(nm);box.appendChild(row);});\n view.appendChild(box);\n view.appendChild(pathBar(t.path));\n}\nfunction renderLifecycle(view){\n const L=DATA.lifecycle;\n document.getElementById('foot').textContent=\"Lifecycle & gates \u00B7 source: \"+L.source;\n view.appendChild(intro(L.summary));\n view.appendChild(ghdr(\"Rule strength \u2014 prefer enforceable over remembered\"));\n const lad=el('div','ladder');\n L.rule_strength.forEach((r,i)=>{const lv=el('div','lv');lv.appendChild(Object.assign(el('b'),{textContent:(i+1)+\". \"+r.level}));lv.appendChild(el('span','d',r.detail));lad.appendChild(lv);});\n view.appendChild(lad);\n view.appendChild(ghdr(\"Scope lifecycle\"));\n const flow=el('div','flow');\n L.scope_states.forEach((s,i)=>{if(i)flow.appendChild(el('div','arw','\u2192'));\n const n=el('div','node');n.appendChild(el('div','st',s.state));n.appendChild(el('div','via',s.via));flow.appendChild(n);});\n view.appendChild(flow);\n view.appendChild(ghdr(\"vBRIEF \u2014 the durable state\"));\n const box=el('div','rows');\n L.vbrief_files.forEach(v=>{const row=el('div','row');row.style.cursor='default';\n const nm=el('div','rname');nm.appendChild(Object.assign(el('code'),{textContent:v.file}));nm.appendChild(el('span','sub',v.role));row.appendChild(nm);box.appendChild(row);});\n view.appendChild(box);\n view.appendChild(ghdr(\"Quality gates\"));\n const g=el('div');g.style.marginBottom='14px';\n L.gates.forEach(x=>{const p=el('span','pill');p.appendChild(Object.assign(el('code'),{textContent:x}));g.appendChild(p);});\n view.appendChild(g);\n view.appendChild(ghdr(\"Key docs\"));\n const kd=el('div','rows');\n L.key_docs.forEach(doc=>{const row=el('div','row');row.style.cursor='default';\n const nm=el('div','rname');nm.appendChild(document.createTextNode(doc.title));nm.appendChild(el('span','sub',doc.path));row.appendChild(nm);\n const b=el('button','act ghost','Copy path');b.onclick=e=>{e.stopPropagation();copyPath(doc.path);};\n const rt=el('div','rt');rt.appendChild(b);row.appendChild(rt);kd.appendChild(row);});\n view.appendChild(kd);\n}\nfunction renderSearch(view){\n const q=document.getElementById('q').value.trim().toLowerCase();\n const hits=[];\n const push=(label,sub,onclick,tag)=>hits.push({label,sub,onclick,tag});\n DATA.groupings.forEach(g=>{\n if((g.name+\" \"+g.purpose).toLowerCase().includes(q))\n push(g.name,g.purpose,()=>{spine='rules';stack=[{label:g.name,render:v=>groupView(v,g)}];document.getElementById('q').value='';render();},\"grouping\");\n g.items.forEach(it=>{\n const scan=f=>{if((f.title+\" \"+f.name+\" \"+(f.summary||\"\")).toLowerCase().includes(q))\n push(f.title||f.name,f.summary||g.name,()=>{spine='rules';stack=[{label:g.name,render:v=>groupView(v,g)},{label:f.name,render:v=>docDetail(v,f)}];document.getElementById('q').value='';render();},g.name);};\n if(it.kind==='file')scan(it); else{if(it.readme)scan(it.readme);it.files.forEach(scan);}\n });\n });\n DATA.tasks.forEach(t=>{\n if((t.namespace+\" \"+t.purpose).toLowerCase().includes(q))\n push(\"task \"+t.namespace,t.purpose,()=>{spine='tasks';stack=[{label:t.namespace,render:v=>taskDetail(v,t)}];document.getElementById('q').value='';render();},\"tasks\");\n t.tasks.forEach(tk=>{if((tk.name+\" \"+tk.desc).toLowerCase().includes(q))\n push(\"task \"+t.namespace+\":\"+tk.name,tk.desc,()=>{spine='tasks';stack=[{label:t.namespace,render:v=>taskDetail(v,t)}];document.getElementById('q').value='';render();},\"task\");});\n });\n document.getElementById('foot').textContent=hits.length+\" matches\";\n if(!hits.length){view.appendChild(el('div','empty','No matches for \"'+q+'\"'));return;}\n const box=el('div','rows');\n hits.slice(0,120).forEach(h=>{const row=el('div','row');\n const nm=el('div','rname');nm.appendChild(document.createTextNode(h.label));if(h.sub)nm.appendChild(el('span','sub',h.sub));row.appendChild(nm);\n if(h.tag){const rt=el('div','rt');rt.appendChild(el('span','pill',h.tag));row.appendChild(rt);}\n row.appendChild(el('span','chev','\u203A'));row.onclick=h.onclick;box.appendChild(row);});\n view.appendChild(box);\n}\n\ndocument.querySelectorAll('nav.spines button').forEach(b=>b.onclick=()=>reset(b.dataset.spine));\nconst qEl=document.getElementById('q');\nqEl.addEventListener('input',render);\nqEl.addEventListener('keydown',e=>{if(e.key==='Escape'){qEl.value='';render();}});\n\nif(!DATA){document.getElementById('view').innerHTML='<div class=\"empty\">No data embedded. Rebuild with <code>task docs:rule-map</code>.</div>';}\nelse{document.getElementById('sub').textContent=\"Deft Directive \u00B7 \"+DATA.groupings.length+\" rule groups \u00B7 \"+DATA.tasks.length+\" task namespaces \u00B7 maintainer map\";render();}\n</script>\n</body>\n</html>\n";
2
+ //# sourceMappingURL=rule-map-template.d.ts.map