@deftai/directive-core 0.69.0 → 0.71.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 (103) hide show
  1. package/dist/branch/evaluate.js +7 -0
  2. package/dist/deposit/contain.d.ts +41 -0
  3. package/dist/deposit/contain.js +113 -0
  4. package/dist/deposit/copy-tree.js +8 -2
  5. package/dist/doctor/checks.d.ts +11 -0
  6. package/dist/doctor/checks.js +75 -2
  7. package/dist/doctor/doctor-state.js +2 -2
  8. package/dist/doctor/main.js +54 -1
  9. package/dist/doctor/manifest.d.ts +20 -0
  10. package/dist/doctor/manifest.js +22 -0
  11. package/dist/doctor/types.d.ts +8 -0
  12. package/dist/eval/crud-telemetry.d.ts +59 -0
  13. package/dist/eval/crud-telemetry.js +307 -0
  14. package/dist/eval/health.d.ts +52 -0
  15. package/dist/eval/health.js +240 -0
  16. package/dist/eval/readback.d.ts +38 -0
  17. package/dist/eval/readback.js +209 -0
  18. package/dist/eval/report.d.ts +53 -0
  19. package/dist/eval/report.js +161 -0
  20. package/dist/eval/run.d.ts +79 -0
  21. package/dist/eval/run.js +309 -0
  22. package/dist/events/attribution-constants.d.ts +13 -0
  23. package/dist/events/attribution-constants.js +18 -0
  24. package/dist/events/attribution-ledger.d.ts +43 -0
  25. package/dist/events/attribution-ledger.js +61 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.js +5 -0
  28. package/dist/init-deposit/gitignore.js +5 -5
  29. package/dist/init-deposit/init-deposit.js +6 -0
  30. package/dist/init-deposit/refresh.js +5 -0
  31. package/dist/intake/issue-ingest.d.ts +11 -0
  32. package/dist/intake/issue-ingest.js +86 -11
  33. package/dist/layout/resolve.d.ts +2 -2
  34. package/dist/layout/resolve.js +2 -2
  35. package/dist/lifecycle/events.js +2 -0
  36. package/dist/policy/index.d.ts +2 -1
  37. package/dist/policy/index.js +15 -2
  38. package/dist/policy/plan-extensions.d.ts +31 -0
  39. package/dist/policy/plan-extensions.js +45 -0
  40. package/dist/policy/value-feedback.d.ts +56 -0
  41. package/dist/policy/value-feedback.js +284 -0
  42. package/dist/policy/wip.d.ts +2 -2
  43. package/dist/policy/wip.js +2 -2
  44. package/dist/preflight-cache/evaluate.js +4 -3
  45. package/dist/scope/audit-log.js +3 -3
  46. package/dist/scope/transition.js +16 -5
  47. package/dist/session/session-start.js +20 -0
  48. package/dist/slice/record.js +3 -3
  49. package/dist/swarm/routing.js +10 -3
  50. package/dist/triage/actions/candidates-log.d.ts +4 -4
  51. package/dist/triage/actions/candidates-log.js +4 -7
  52. package/dist/triage/bootstrap/gitignore.d.ts +7 -3
  53. package/dist/triage/bootstrap/gitignore.js +67 -37
  54. package/dist/triage/bootstrap/index.d.ts +1 -1
  55. package/dist/triage/bootstrap/index.js +4 -3
  56. package/dist/triage/bulk/index.d.ts +11 -2
  57. package/dist/triage/bulk/index.js +43 -6
  58. package/dist/triage/cache-path.d.ts +39 -0
  59. package/dist/triage/cache-path.js +98 -0
  60. package/dist/triage/help/registry-data.d.ts +53 -20
  61. package/dist/triage/help/registry-data.js +104 -20
  62. package/dist/triage/index.d.ts +2 -0
  63. package/dist/triage/index.js +2 -0
  64. package/dist/triage/queue/audit.js +2 -2
  65. package/dist/triage/queue/cache.js +2 -2
  66. package/dist/triage/reconcile/reconcile.js +4 -3
  67. package/dist/triage/scope/cli.d.ts +2 -1
  68. package/dist/triage/scope/cli.js +30 -6
  69. package/dist/triage/scope/mutations-core.js +3 -2
  70. package/dist/triage/scope/mutations.d.ts +10 -0
  71. package/dist/triage/scope/mutations.js +22 -0
  72. package/dist/triage/subscribe/index.d.ts +1 -1
  73. package/dist/triage/subscribe/index.js +4 -3
  74. package/dist/triage/summary/index.d.ts +2 -2
  75. package/dist/triage/summary/index.js +4 -3
  76. package/dist/triage/summary/reconcilable.d.ts +1 -1
  77. package/dist/triage/summary/reconcilable.js +3 -2
  78. package/dist/triage/welcome/constants.d.ts +3 -4
  79. package/dist/triage/welcome/constants.js +3 -4
  80. package/dist/triage/welcome/index.d.ts +1 -0
  81. package/dist/triage/welcome/index.js +1 -0
  82. package/dist/triage/welcome/onboard.d.ts +35 -0
  83. package/dist/triage/welcome/onboard.js +94 -0
  84. package/dist/triage/welcome/prior-state.js +3 -2
  85. package/dist/triage/welcome/summary.js +5 -4
  86. package/dist/umbrella-current-shape/index.d.ts +25 -1
  87. package/dist/umbrella-current-shape/index.js +84 -4
  88. package/dist/value/adoption-emit.d.ts +14 -0
  89. package/dist/value/adoption-emit.js +67 -0
  90. package/dist/value/adoption-registry.d.ts +62 -0
  91. package/dist/value/adoption-registry.js +214 -0
  92. package/dist/value/feedback-file.d.ts +67 -0
  93. package/dist/value/feedback-file.js +353 -0
  94. package/dist/value/friction-emit.d.ts +10 -0
  95. package/dist/value/friction-emit.js +24 -0
  96. package/dist/value/readback.d.ts +86 -0
  97. package/dist/value/readback.js +503 -0
  98. package/dist/vbrief-build/project-definition-io.d.ts +6 -0
  99. package/dist/vbrief-build/project-definition-io.js +7 -2
  100. package/dist/vbrief-validate/decomposition.js +1 -1
  101. package/dist/vbrief-validate/precutover.js +7 -3
  102. package/dist/wip-cap/evaluate.js +3 -0
  103. package/package.json +35 -3
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { cacheGet } from "../cache/operations.js";
4
+ import { scan } from "../cache/scanner.js";
4
5
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
5
6
  import { call } from "../scm/call.js";
6
7
  import { resolveProjectRoot } from "../scope/project-context.js";
@@ -11,6 +12,27 @@ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LEGACY_VBRIEF_VERSION, MI
11
12
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
12
13
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
13
14
  export const INGEST_STATUSES = ["proposed", "pending", "active"];
15
+ /**
16
+ * Thrown when the quarantine scanner hard-fails (credential-shaped content) on
17
+ * an ingested issue body/comment thread (#2306). Ingest MUST fail closed: emit
18
+ * nothing and propagate a non-zero exit rather than persisting the xBRIEF.
19
+ */
20
+ export class ScannerHardFailError extends Error {
21
+ issueNumber;
22
+ flags;
23
+ constructor(issueNumber, flags) {
24
+ const details = flags
25
+ .filter((f) => f.severity === "hard-fail")
26
+ .map((f) => f.detail)
27
+ .join("; ");
28
+ super(`issue:ingest refused #${issueNumber}: quarantine scanner hard-fail` +
29
+ (details.length > 0 ? ` (${details})` : "") +
30
+ " -- nothing written.");
31
+ this.name = "ScannerHardFailError";
32
+ this.issueNumber = issueNumber;
33
+ this.flags = flags;
34
+ }
35
+ }
14
36
  /** Enriched on issues after `fetchIssue` when the comment thread is non-empty (#2143). */
15
37
  export const ISSUE_COMMENT_THREAD_KEY = "issueCommentThread";
16
38
  const STATUS_MAP = {
@@ -311,7 +333,14 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
311
333
  };
312
334
  if (overviewSource.length > 0) {
313
335
  warnBodyControlCharacters(number, overviewSource);
314
- narratives.Overview = overviewSource;
336
+ // #2306: quarantine-scan untrusted body + comment-thread content before it
337
+ // is persisted as agent-facing scope authority. Fail closed on a credential
338
+ // hard-fail; otherwise persist the fenced/quarantined transform.
339
+ const scanResult = scan(overviewSource);
340
+ if (!scanResult.passed) {
341
+ throw new ScannerHardFailError(number, scanResult.flags);
342
+ }
343
+ narratives.Overview = scanResult.transformed_content;
315
344
  }
316
345
  if (labelNames.length > 0) {
317
346
  narratives.Labels = labelNames.join(", ");
@@ -369,6 +398,19 @@ export function fetchFromCache(repo, number, options = {}) {
369
398
  if (typeof issue.html_url === "string" && issue.html_url.length > 0) {
370
399
  issue.url = issue.html_url;
371
400
  }
401
+ // #2306: consume the cache entry's SCANNED content.md (fenced/quarantined at
402
+ // cache-put) rather than the raw body, so the cache read path cannot bypass
403
+ // the quarantine transform. When content.md is absent (e.g. a credential
404
+ // hard-fail deleted it), the raw body falls through and is re-scanned in
405
+ // buildIssueVbrief.
406
+ if (result.contentPath !== null) {
407
+ try {
408
+ issue.body = readFileSync(result.contentPath, "utf8");
409
+ }
410
+ catch {
411
+ // fall back to the raw body (re-scanned downstream)
412
+ }
413
+ }
372
414
  return issue;
373
415
  }
374
416
  catch {
@@ -514,9 +556,25 @@ export function ingestBulk(issues, options) {
514
556
  created: [],
515
557
  duplicate: [],
516
558
  dryrun: [],
559
+ failed: [],
517
560
  };
518
561
  for (const issue of filtered) {
519
- const [result, path, _msg] = ingestOne(issue, { ...options, existingRefs: refs });
562
+ let ingested;
563
+ try {
564
+ ingested = ingestOne(issue, { ...options, existingRefs: refs });
565
+ }
566
+ catch (exc) {
567
+ // #2306: a per-issue quarantine hard-fail must not sink the whole batch;
568
+ // record it, emit nothing for that issue, and surface a non-zero exit
569
+ // upstream via the `failed` bucket.
570
+ if (exc instanceof ScannerHardFailError) {
571
+ summary.failed.push(`#${exc.issueNumber}`);
572
+ process.stderr.write(`${exc.message}\n`);
573
+ continue;
574
+ }
575
+ throw exc;
576
+ }
577
+ const [result, path, _msg] = ingested;
520
578
  const rel = path !== null ? path.replace(`${options.vbriefDir}/`, "").replace(/\\/g, "/") : "";
521
579
  summary[result].push(rel);
522
580
  if (result === "created" && path !== null) {
@@ -581,7 +639,8 @@ export function issueIngestMain(args) {
581
639
  const created = summary.created;
582
640
  const duplicate = summary.duplicate;
583
641
  const dryrun = summary.dryrun;
584
- process.stdout.write(`issue:ingest bulk summary: ${created.length} created, ${duplicate.length} duplicate, ${dryrun.length} dry-run (total considered: ${summary.total})\n`);
642
+ const failed = summary.failed ?? [];
643
+ process.stdout.write(`issue:ingest bulk summary: ${created.length} created, ${duplicate.length} duplicate, ${dryrun.length} dry-run, ${failed.length} refused (total considered: ${summary.total})\n`);
585
644
  for (const entry of created) {
586
645
  process.stdout.write(` CREATED ${entry}\n`);
587
646
  }
@@ -591,19 +650,35 @@ export function issueIngestMain(args) {
591
650
  for (const entry of duplicate) {
592
651
  process.stdout.write(` SKIP ${entry} (already has scope vBRIEF)\n`);
593
652
  }
594
- return 0;
653
+ for (const entry of failed) {
654
+ process.stdout.write(` REFUSED ${entry} (quarantine scanner hard-fail; nothing written)\n`);
655
+ }
656
+ // #2306: fail closed on any quarantine hard-fail in the batch.
657
+ return failed.length > 0 ? 2 : 0;
595
658
  }
596
659
  const issue = fetchIssue(repo, args.number, { cwd: projectRoot });
597
660
  if (issue === null) {
598
661
  return 2;
599
662
  }
600
- const [result, _path, msg] = ingestOne(issue, {
601
- vbriefDir,
602
- status,
603
- repoUrl,
604
- dryRun: args.dryRun,
605
- cwd: projectRoot,
606
- });
663
+ let result;
664
+ let msg;
665
+ try {
666
+ [result, , msg] = ingestOne(issue, {
667
+ vbriefDir,
668
+ status,
669
+ repoUrl,
670
+ dryRun: args.dryRun,
671
+ cwd: projectRoot,
672
+ });
673
+ }
674
+ catch (exc) {
675
+ // #2306: fail closed -- emit nothing, non-zero exit on a quarantine hard-fail.
676
+ if (exc instanceof ScannerHardFailError) {
677
+ process.stderr.write(`${exc.message}\n`);
678
+ return 2;
679
+ }
680
+ throw exc;
681
+ }
607
682
  process.stdout.write(`${msg}\n`);
608
683
  return result === "duplicate" ? 1 : 0;
609
684
  }
@@ -51,9 +51,9 @@ export declare function resolveLifecycleRoot(projectRoot: string): string;
51
51
  * (e.g. `<root>/<xbrief|vbrief>/active`). Layout-aware (#2109 part 2a).
52
52
  */
53
53
  export declare function resolveLifecycleFolder(projectRoot: string, folder: string): string;
54
- /** Absolute path to the resolved lifecycle `.eval` cache directory (#2109 part 2a). */
54
+ /** Absolute path to the layout-aware `.eval/` directory for version-eval results (#1703). */
55
55
  export declare function resolveEvalDir(projectRoot: string): string;
56
- /** Absolute path to a file or subpath under the resolved `.eval` cache directory. */
56
+ /** Absolute path under `.eval/` for version-eval artefacts (not triage working-set). */
57
57
  export declare function resolveEvalPath(projectRoot: string, ...segments: string[]): string;
58
58
  /** Absolute path to the resolved lifecycle `.audit` directory (#2109 part 2a). */
59
59
  export declare function resolveAuditDir(projectRoot: string): string;
@@ -101,11 +101,11 @@ export function resolveLifecycleRoot(projectRoot) {
101
101
  export function resolveLifecycleFolder(projectRoot, folder) {
102
102
  return join(resolveLifecycleRoot(projectRoot), folder);
103
103
  }
104
- /** Absolute path to the resolved lifecycle `.eval` cache directory (#2109 part 2a). */
104
+ /** Absolute path to the layout-aware `.eval/` directory for version-eval results (#1703). */
105
105
  export function resolveEvalDir(projectRoot) {
106
106
  return join(resolveLifecycleRoot(projectRoot), ".eval");
107
107
  }
108
- /** Absolute path to a file or subpath under the resolved `.eval` cache directory. */
108
+ /** Absolute path under `.eval/` for version-eval artefacts (not triage working-set). */
109
109
  export function resolveEvalPath(projectRoot, ...segments) {
110
110
  return join(resolveEvalDir(projectRoot), ...segments);
111
111
  }
@@ -3,10 +3,12 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { contentRoot } from "../content-root.js";
6
+ import { ATTRIBUTION_REQUIRED_PAYLOAD } from "../events/attribution-constants.js";
6
7
  /** Default event log location (project-local). */
7
8
  export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
8
9
  const BEHAVIORAL_CATEGORY = "behavioral";
9
10
  const REQUIRED_BEHAVIORAL_PAYLOAD = {
11
+ ...ATTRIBUTION_REQUIRED_PAYLOAD,
10
12
  "session:interrupted": ["session_id", "reason"],
11
13
  "session:resumed": ["session_id", "interrupted_id"],
12
14
  "plan:approved": ["plan_ref", "approver"],
@@ -5,6 +5,7 @@ export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
6
  export * from "./plan-extensions.js";
7
7
  export * from "./resolve.js";
8
+ export * from "./value-feedback.js";
8
9
  export * from "./wip.js";
9
10
  export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
10
11
  export declare const FIELD_WIP_CAP = "plan.policy.wipCap";
@@ -29,7 +30,7 @@ export interface PolicyField {
29
30
  }
30
31
  /** Walk registered inspectors and return one row per field (#1148). */
31
32
  export declare function inspectAllPolicies(projectRoot: string): PolicyField[];
32
- /** Look up a single registered field by canonical dotted-path name. */
33
+ /** Look up a single registered field by canonical dotted-path name (or CLI alias). */
33
34
  export declare function inspectOnePolicy(name: string, projectRoot: string): PolicyField | null;
34
35
  /** Return canonical names of every registered typed-policy field. */
35
36
  export declare function registeredPolicyNames(): string[];
@@ -1,5 +1,6 @@
1
1
  import { readPlanPolicy } from "./plan-extensions.js";
2
2
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
3
+ import { FIELD_VALUE_FEEDBACK, FIELD_VALUE_FEEDBACK_CLI_ALIAS, inspectValueFeedback, } from "./value-feedback.js";
3
4
  import { DEFAULT_WIP_CAP } from "./wip.js";
4
5
  export * from "./agents-md-advisory.js";
5
6
  export * from "./autonomy.js";
@@ -8,6 +9,7 @@ export * from "./decisions.js";
8
9
  export * from "./disclosure.js";
9
10
  export * from "./plan-extensions.js";
10
11
  export * from "./resolve.js";
12
+ export * from "./value-feedback.js";
11
13
  export * from "./wip.js";
12
14
  export const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
13
15
  export const FIELD_WIP_CAP = "plan.policy.wipCap";
@@ -213,6 +215,15 @@ function inspectSwarmSubagentBackend(data) {
213
215
  source: "default-on-error",
214
216
  };
215
217
  }
218
+ function inspectValueFeedbackField(data) {
219
+ const field = inspectValueFeedback(data);
220
+ return {
221
+ name: field.name,
222
+ current: field.current,
223
+ default: field.default,
224
+ source: field.source,
225
+ };
226
+ }
216
227
  const REGISTERED_POLICIES = [
217
228
  inspectAllowDirectCommits,
218
229
  inspectWipCap,
@@ -225,16 +236,18 @@ const REGISTERED_POLICIES = [
225
236
  emptyIsTyped: true,
226
237
  }),
227
238
  inspectSwarmSubagentBackend,
239
+ inspectValueFeedbackField,
228
240
  ];
229
241
  /** Walk registered inspectors and return one row per field (#1148). */
230
242
  export function inspectAllPolicies(projectRoot) {
231
243
  const [data] = loadProjectDefinition(projectRoot);
232
244
  return REGISTERED_POLICIES.map((inspect) => inspect(data));
233
245
  }
234
- /** Look up a single registered field by canonical dotted-path name. */
246
+ /** Look up a single registered field by canonical dotted-path name (or CLI alias). */
235
247
  export function inspectOnePolicy(name, projectRoot) {
248
+ const normalized = name === FIELD_VALUE_FEEDBACK_CLI_ALIAS ? FIELD_VALUE_FEEDBACK : name;
236
249
  for (const field of inspectAllPolicies(projectRoot)) {
237
- if (field.name === name)
250
+ if (field.name === normalized)
238
251
  return field;
239
252
  }
240
253
  return null;
@@ -32,4 +32,35 @@ export declare function readPlanCompletedNote(plan: unknown): unknown;
32
32
  * bare block alongside the namespaced one.
33
33
  */
34
34
  export declare function migrateLegacyPolicyKey(plan: Record<string, unknown>): void;
35
+ /** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
36
+ export declare const SHADOWABLE_PLAN_EXTENSIONS: ReadonlyArray<{
37
+ readonly namespacedKey: string;
38
+ readonly legacyKey: string;
39
+ }>;
40
+ /** A plan-extension key whose bare form is silently shadowed by the namespaced form. */
41
+ export interface ShadowedPlanExtension {
42
+ /** The namespaced key that wins the read (e.g. `x-directive/policy`). */
43
+ readonly namespacedKey: string;
44
+ /** The bare legacy key that is silently ignored (e.g. `policy`). */
45
+ readonly legacyKey: string;
46
+ /**
47
+ * Best-effort list of sub-keys present in the shadowed bare object (e.g.
48
+ * `["triageScope", "wipCap"]`). Empty when the bare value is not an object.
49
+ */
50
+ readonly shadowedSubKeys: readonly string[];
51
+ }
52
+ /**
53
+ * Detect every plan-extension key where a bare (legacy) block coexists with the
54
+ * namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
55
+ * bare block is never read once the namespaced key exists -- edits to it take no
56
+ * effect. Detecting the coexistence lets callers emit a loud diagnostic instead
57
+ * of the silent no-op that the #2295 onboarding trap exhibited.
58
+ */
59
+ export declare function detectShadowedPlanExtensions(plan: unknown): ShadowedPlanExtension[];
60
+ /**
61
+ * Render a human-readable, loud diagnostic for a single shadowed plan-extension
62
+ * key. The message is surface-agnostic (no leading tag) so each caller can
63
+ * prefix it (`[policy:show]`, a doctor finding, ...).
64
+ */
65
+ export declare function describeShadowedPlanExtension(shadow: ShadowedPlanExtension): string;
35
66
  //# sourceMappingURL=plan-extensions.d.ts.map
@@ -54,4 +54,49 @@ export function migrateLegacyPolicyKey(plan) {
54
54
  delete plan[LEGACY_PLAN_POLICY_KEY];
55
55
  }
56
56
  }
57
+ /** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
58
+ export const SHADOWABLE_PLAN_EXTENSIONS = [
59
+ { namespacedKey: PLAN_POLICY_KEY, legacyKey: LEGACY_PLAN_POLICY_KEY },
60
+ { namespacedKey: PLAN_COMPLETED_NOTE_KEY, legacyKey: LEGACY_PLAN_COMPLETED_NOTE_KEY },
61
+ ];
62
+ /**
63
+ * Detect every plan-extension key where a bare (legacy) block coexists with the
64
+ * namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
65
+ * bare block is never read once the namespaced key exists -- edits to it take no
66
+ * effect. Detecting the coexistence lets callers emit a loud diagnostic instead
67
+ * of the silent no-op that the #2295 onboarding trap exhibited.
68
+ */
69
+ export function detectShadowedPlanExtensions(plan) {
70
+ const planObj = asPlanObject(plan);
71
+ if (planObj === null) {
72
+ return [];
73
+ }
74
+ const shadows = [];
75
+ for (const { namespacedKey, legacyKey } of SHADOWABLE_PLAN_EXTENSIONS) {
76
+ if (planObj[namespacedKey] === undefined || planObj[legacyKey] === undefined) {
77
+ continue;
78
+ }
79
+ const legacyValue = planObj[legacyKey];
80
+ const shadowedSubKeys = typeof legacyValue === "object" && legacyValue !== null && !Array.isArray(legacyValue)
81
+ ? Object.keys(legacyValue)
82
+ : [];
83
+ shadows.push({ namespacedKey, legacyKey, shadowedSubKeys });
84
+ }
85
+ return shadows;
86
+ }
87
+ /**
88
+ * Render a human-readable, loud diagnostic for a single shadowed plan-extension
89
+ * key. The message is surface-agnostic (no leading tag) so each caller can
90
+ * prefix it (`[policy:show]`, a doctor finding, ...).
91
+ */
92
+ export function describeShadowedPlanExtension(shadow) {
93
+ const subKeys = shadow.shadowedSubKeys.length > 0
94
+ ? ` Shadowed field(s): ${shadow.shadowedSubKeys
95
+ .map((k) => `plan.${shadow.legacyKey}.${k}`)
96
+ .join(", ")}.`
97
+ : "";
98
+ return (`bare \`plan.${shadow.legacyKey}\` coexists with namespaced \`plan.${shadow.namespacedKey}\`; ` +
99
+ `the bare block is IGNORED (namespaced-first read) so edits to it silently take no effect.${subKeys} ` +
100
+ `Fold its values into \`plan.${shadow.namespacedKey}\` and delete \`plan.${shadow.legacyKey}\`.`);
101
+ }
57
102
  //# sourceMappingURL=plan-extensions.js.map
@@ -0,0 +1,56 @@
1
+ /** Canonical registered policy field name (matches other FIELD_* dotted paths). */
2
+ export declare const FIELD_VALUE_FEEDBACK = "plan.policy.valueFeedback";
3
+ /** Short alias accepted by `policy:show --field=valueFeedback` (#1709). */
4
+ export declare const FIELD_VALUE_FEEDBACK_CLI_ALIAS = "valueFeedback";
5
+ export declare const DEFAULT_VALUE_FEEDBACK_ENABLED = false;
6
+ /** Sub-flag defaults applied when the master flag is enabled (#1709 tiered-cost decision). */
7
+ export declare const VALUE_FEEDBACK_SUBFLAG_DEFAULTS_WHEN_ENABLED: {
8
+ readonly emitEvents: true;
9
+ readonly sessionLine: true;
10
+ readonly upstreamPrompt: false;
11
+ };
12
+ export type ValueFeedbackSubFlag = keyof typeof VALUE_FEEDBACK_SUBFLAG_DEFAULTS_WHEN_ENABLED;
13
+ export interface ValueFeedbackConfig {
14
+ readonly enabled: boolean;
15
+ readonly emitEvents: boolean;
16
+ readonly sessionLine: boolean;
17
+ readonly upstreamPrompt: boolean;
18
+ }
19
+ export type ValueFeedbackSource = "typed" | "default" | "default-on-error";
20
+ export interface ValueFeedbackResolved extends ValueFeedbackConfig {
21
+ readonly source: ValueFeedbackSource;
22
+ readonly error: string | null;
23
+ }
24
+ export declare const VALUE_FEEDBACK_CAPABILITY_COST_DISCLOSURE: string;
25
+ /** Validate a `plan.policy.valueFeedback` payload. */
26
+ export declare function validateValueFeedback(value: unknown): string[];
27
+ /** Resolve `plan.policy.valueFeedback` from PROJECT-DEFINITION (#1709). */
28
+ export declare function resolveValueFeedback(projectRoot: string): ValueFeedbackResolved;
29
+ /** Master gate: when `enabled` is false, every downstream path is rejected. */
30
+ export declare function isValueFeedbackPathAllowed(path: ValueFeedbackSubFlag, policy: ValueFeedbackResolved): boolean;
31
+ /** Resolved per-path gate booleans for policy:show and enable status output. */
32
+ export declare function valueFeedbackPathGates(policy: ValueFeedbackResolved): Record<ValueFeedbackSubFlag, boolean>;
33
+ /** Human-readable status line for CLI enable/show surfaces. */
34
+ export declare function formatValueFeedbackStatusLine(policy: ValueFeedbackResolved): string;
35
+ export interface ValueFeedbackPolicyField {
36
+ readonly name: typeof FIELD_VALUE_FEEDBACK;
37
+ readonly current: ValueFeedbackConfig;
38
+ readonly default: ValueFeedbackConfig;
39
+ readonly source: string;
40
+ }
41
+ /** Inspector row for `policy:show --field=valueFeedback`. */
42
+ export declare function inspectValueFeedback(data: Record<string, unknown> | null): ValueFeedbackPolicyField;
43
+ export interface EnableValueFeedbackOptions {
44
+ readonly confirm: boolean;
45
+ readonly actor?: string;
46
+ readonly note?: string;
47
+ readonly subFlags?: Partial<Record<ValueFeedbackSubFlag, boolean>>;
48
+ }
49
+ export interface EnableValueFeedbackResult {
50
+ readonly exitCode: 0 | 1 | 2;
51
+ readonly stdout: string;
52
+ readonly changed: boolean;
53
+ }
54
+ /** Persist `valueFeedback.enabled=true` after capability-cost disclosure (#1709). */
55
+ export declare function enableValueFeedback(projectRoot: string, options: EnableValueFeedbackOptions): EnableValueFeedbackResult;
56
+ //# sourceMappingURL=value-feedback.d.ts.map