@deftai/directive-core 0.80.0 → 0.81.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 (32) hide show
  1. package/dist/doctor/checks.d.ts +6 -0
  2. package/dist/doctor/checks.js +139 -0
  3. package/dist/doctor/main.js +2 -1
  4. package/dist/policy/index.d.ts +1 -0
  5. package/dist/policy/index.js +19 -5
  6. package/dist/policy/product-signal.d.ts +45 -0
  7. package/dist/policy/product-signal.js +210 -0
  8. package/dist/product-signal/actor-name.d.ts +18 -0
  9. package/dist/product-signal/actor-name.js +94 -0
  10. package/dist/product-signal/consent.d.ts +30 -0
  11. package/dist/product-signal/consent.js +116 -0
  12. package/dist/product-signal/gates.d.ts +17 -0
  13. package/dist/product-signal/gates.js +66 -0
  14. package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
  15. package/dist/product-signal/github-private-sink-adapter.js +274 -0
  16. package/dist/product-signal/headless.d.ts +8 -0
  17. package/dist/product-signal/headless.js +32 -0
  18. package/dist/product-signal/install-context.d.ts +13 -0
  19. package/dist/product-signal/install-context.js +41 -0
  20. package/dist/product-signal/local-signal-summary.d.ts +7 -0
  21. package/dist/product-signal/local-signal-summary.js +173 -0
  22. package/dist/product-signal/payload.d.ts +90 -0
  23. package/dist/product-signal/payload.js +124 -0
  24. package/dist/product-signal/sink-bootstrap.d.ts +29 -0
  25. package/dist/product-signal/sink-bootstrap.js +108 -0
  26. package/dist/product-signal/submit-adapter.d.ts +14 -0
  27. package/dist/product-signal/submit-adapter.js +2 -0
  28. package/dist/product-signal/submit.d.ts +54 -0
  29. package/dist/product-signal/submit.js +290 -0
  30. package/dist/scm/gh-rest.d.ts +3 -1
  31. package/dist/scm/gh-rest.js +22 -0
  32. package/package.json +3 -3
@@ -39,6 +39,12 @@ export declare const STALE_VBRIEF_CORE_SCHEMA_REL: string;
39
39
  * `directive update` instead. Advisory only (exit-exempt).
40
40
  */
41
41
  export declare function checkStaleXbriefSchemaDeposit(projectRoot: string, seams?: CheckSeams): CheckResult;
42
+ /**
43
+ * #2591: advisory hint when a TypeScript project pins bare `typescript@7` alongside
44
+ * typescript-eslint without the `@typescript/typescript6` alias. Exit-exempt (like
45
+ * gitignore-coverage) — adoption guidance, not a broken install.
46
+ */
47
+ export declare function checkTypescript7SideBySide(projectRoot: string, seams?: CheckSeams): CheckResult;
42
48
  /**
43
49
  * #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
44
50
  * entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
@@ -523,6 +523,142 @@ export function checkStaleXbriefSchemaDeposit(projectRoot, seams = {}) {
523
523
  },
524
524
  };
525
525
  }
526
+ const TS7_SIDE_BY_SIDE_CHECK = "typescript-7-side-by-side";
527
+ function depKeyIsTypescriptEslint(key) {
528
+ return key === "typescript-eslint" || key.startsWith("@typescript-eslint/");
529
+ }
530
+ function depKeysIncludeTypescriptEslint(keys) {
531
+ return keys.some(depKeyIsTypescriptEslint);
532
+ }
533
+ function depKeysIncludeEslint(keys) {
534
+ return keys.includes("eslint");
535
+ }
536
+ function typescriptValueIs7Bound(value) {
537
+ const v = value.trim();
538
+ if (!v || v.includes("@typescript/typescript6")) {
539
+ return false;
540
+ }
541
+ if (/npm:typescript@(?:[\^~>=<]*|\d*)7/i.test(v)) {
542
+ return true;
543
+ }
544
+ if (/^[\^~>=<]*7(?:\.\d|$)/.test(v)) {
545
+ return true;
546
+ }
547
+ if (/^7\.\d/.test(v)) {
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ function resolveTypescriptDepValue(deps, devDeps) {
553
+ if (typeof devDeps.typescript === "string") {
554
+ return devDeps.typescript;
555
+ }
556
+ if (typeof deps.typescript === "string") {
557
+ return deps.typescript;
558
+ }
559
+ return null;
560
+ }
561
+ /**
562
+ * #2591: advisory hint when a TypeScript project pins bare `typescript@7` alongside
563
+ * typescript-eslint without the `@typescript/typescript6` alias. Exit-exempt (like
564
+ * gitignore-coverage) — adoption guidance, not a broken install.
565
+ */
566
+ export function checkTypescript7SideBySide(projectRoot, seams = {}) {
567
+ const packageJsonPath = join(projectRoot, "package.json");
568
+ const text = readText(packageJsonPath, seams);
569
+ if (text === null) {
570
+ return {
571
+ name: TS7_SIDE_BY_SIDE_CHECK,
572
+ status: "skip",
573
+ detail: "package.json not found; nothing to inspect for TypeScript side-by-side layout.",
574
+ data: { package_json_path: packageJsonPath },
575
+ };
576
+ }
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(text);
580
+ }
581
+ catch {
582
+ return {
583
+ name: TS7_SIDE_BY_SIDE_CHECK,
584
+ status: "skip",
585
+ detail: "package.json is unreadable; skipping TypeScript side-by-side check.",
586
+ data: { package_json_path: packageJsonPath },
587
+ };
588
+ }
589
+ if (typeof parsed !== "object" || parsed === null) {
590
+ return {
591
+ name: TS7_SIDE_BY_SIDE_CHECK,
592
+ status: "skip",
593
+ detail: "package.json has no dependency sections; skipping TypeScript side-by-side check.",
594
+ data: { package_json_path: packageJsonPath },
595
+ };
596
+ }
597
+ const record = parsed;
598
+ const deps = typeof record.dependencies === "object" && record.dependencies !== null
599
+ ? record.dependencies
600
+ : {};
601
+ const devDeps = typeof record.devDependencies === "object" && record.devDependencies !== null
602
+ ? record.devDependencies
603
+ : {};
604
+ const depKeys = [...Object.keys(deps), ...Object.keys(devDeps)];
605
+ if (depKeys.length === 0) {
606
+ return {
607
+ name: TS7_SIDE_BY_SIDE_CHECK,
608
+ status: "skip",
609
+ detail: "package.json has no dependency sections; skipping TypeScript side-by-side check.",
610
+ data: { package_json_path: packageJsonPath },
611
+ };
612
+ }
613
+ if (!depKeysIncludeTypescriptEslint(depKeys)) {
614
+ return {
615
+ name: TS7_SIDE_BY_SIDE_CHECK,
616
+ status: "pass",
617
+ detail: "No typescript-eslint packages declared; TypeScript 7 side-by-side alias not required.",
618
+ data: { package_json_path: packageJsonPath },
619
+ };
620
+ }
621
+ if (!depKeysIncludeEslint(depKeys)) {
622
+ return {
623
+ name: TS7_SIDE_BY_SIDE_CHECK,
624
+ status: "pass",
625
+ detail: "eslint is not declared; TypeScript 7 side-by-side alias not required.",
626
+ data: { package_json_path: packageJsonPath },
627
+ };
628
+ }
629
+ const typescriptValue = resolveTypescriptDepValue(deps, devDeps);
630
+ if (typescriptValue === null) {
631
+ return {
632
+ name: TS7_SIDE_BY_SIDE_CHECK,
633
+ status: "pass",
634
+ detail: "No typescript dependency declared; TypeScript 7 side-by-side alias not required.",
635
+ data: { package_json_path: packageJsonPath },
636
+ };
637
+ }
638
+ if (!typescriptValueIs7Bound(typescriptValue)) {
639
+ return {
640
+ name: TS7_SIDE_BY_SIDE_CHECK,
641
+ status: "pass",
642
+ detail: "typescript is not pinned to 7.x (or uses the @typescript/typescript6 alias); side-by-side layout not required.",
643
+ data: {
644
+ package_json_path: packageJsonPath,
645
+ typescript: typescriptValue,
646
+ },
647
+ };
648
+ }
649
+ return {
650
+ name: TS7_SIDE_BY_SIDE_CHECK,
651
+ status: "fail",
652
+ detail: "typescript resolves to 7.x without the @typescript/typescript6 alias while typescript-eslint and eslint are present. " +
653
+ "Until TS 7.1, use the side-by-side alias pattern in languages/typescript.md § TypeScript 7 side-by-side (pre-7.1) " +
654
+ "(Cartograph #111: keep typescript on npm:@typescript/typescript6 and install TS 7 under @typescript/native).",
655
+ data: {
656
+ package_json_path: packageJsonPath,
657
+ typescript: typescriptValue,
658
+ suggested_fix: "languages/typescript.md#typescript-7-side-by-side-pre-71",
659
+ },
660
+ };
661
+ }
526
662
  /**
527
663
  * #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
528
664
  * entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
@@ -580,6 +716,7 @@ export function deriveExitCode(checks, errors) {
580
716
  "manifest-version-reportable",
581
717
  "gitignore-coverage",
582
718
  "stale-xbrief-schema-deposit",
719
+ "typescript-7-side-by-side",
583
720
  ]);
584
721
  if (errors.length > 0 || checks.some((c) => c.status === "error")) {
585
722
  return 2;
@@ -621,6 +758,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
621
758
  checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
622
759
  checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
623
760
  checks.push(checkGitignoreCoverage(projectRoot, seams));
761
+ checks.push(checkTypescript7SideBySide(projectRoot, seams));
624
762
  return {
625
763
  projectRoot,
626
764
  installRoot: null,
@@ -638,6 +776,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
638
776
  checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
639
777
  checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
640
778
  checks.push(checkGitignoreCoverage(projectRoot, seams));
779
+ checks.push(checkTypescript7SideBySide(projectRoot, seams));
641
780
  return {
642
781
  projectRoot,
643
782
  installRoot,
@@ -424,7 +424,8 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
424
424
  name === "canonical-vendored-npm-signpost" ||
425
425
  name === "manifest-version-reportable" ||
426
426
  name === "gitignore-coverage" ||
427
- name === "stale-xbrief-schema-deposit") &&
427
+ name === "stale-xbrief-schema-deposit" ||
428
+ name === "typescript-7-side-by-side") &&
428
429
  status === "fail") {
429
430
  sink.warn(`${name}: ${detail}`);
430
431
  addFinding({
@@ -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 "./policy-invocation.js";
8
+ export * from "./product-signal.js";
8
9
  export * from "./resolve.js";
9
10
  export * from "./runtime-authority.js";
10
11
  export * from "./staleness-tickler.js";
@@ -1,4 +1,5 @@
1
1
  import { readPlanPolicy } from "./plan-extensions.js";
2
+ import { FIELD_PRODUCT_SIGNAL, FIELD_PRODUCT_SIGNAL_CLI_ALIAS, inspectProductSignal, } from "./product-signal.js";
2
3
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
3
4
  import { FIELD_RUNTIME_AUTHORITY, FIELD_RUNTIME_AUTHORITY_CLI_ALIAS, inspectRuntimeAuthority, } from "./runtime-authority.js";
4
5
  import { FIELD_STALENESS_TICKLER, FIELD_STALENESS_TICKLER_CLI_ALIAS, inspectStalenessTickler, } from "./staleness-tickler.js";
@@ -11,6 +12,7 @@ export * from "./decisions.js";
11
12
  export * from "./disclosure.js";
12
13
  export * from "./plan-extensions.js";
13
14
  export * from "./policy-invocation.js";
15
+ export * from "./product-signal.js";
14
16
  export * from "./resolve.js";
15
17
  export * from "./runtime-authority.js";
16
18
  export * from "./staleness-tickler.js";
@@ -220,6 +222,15 @@ function inspectSwarmSubagentBackend(data) {
220
222
  source: "default-on-error",
221
223
  };
222
224
  }
225
+ function inspectProductSignalField(data, projectRoot) {
226
+ const field = inspectProductSignal(data, projectRoot);
227
+ return {
228
+ name: field.name,
229
+ current: field.current,
230
+ default: field.default,
231
+ source: field.source,
232
+ };
233
+ }
223
234
  function inspectValueFeedbackField(data, projectRoot) {
224
235
  const field = inspectValueFeedback(data, projectRoot);
225
236
  return {
@@ -261,6 +272,7 @@ const REGISTERED_POLICIES = [
261
272
  inspectSwarmSubagentBackend,
262
273
  inspectStalenessTicklerField,
263
274
  inspectRuntimeAuthorityField,
275
+ inspectProductSignalField,
264
276
  inspectValueFeedbackField,
265
277
  ];
266
278
  /** Walk registered inspectors and return one row per field (#1148). */
@@ -272,11 +284,13 @@ export function inspectAllPolicies(projectRoot) {
272
284
  export function inspectOnePolicy(name, projectRoot) {
273
285
  const normalized = name === FIELD_VALUE_FEEDBACK_CLI_ALIAS
274
286
  ? FIELD_VALUE_FEEDBACK
275
- : name === FIELD_STALENESS_TICKLER_CLI_ALIAS
276
- ? FIELD_STALENESS_TICKLER
277
- : name === FIELD_RUNTIME_AUTHORITY_CLI_ALIAS
278
- ? FIELD_RUNTIME_AUTHORITY
279
- : name;
287
+ : name === FIELD_PRODUCT_SIGNAL_CLI_ALIAS
288
+ ? FIELD_PRODUCT_SIGNAL
289
+ : name === FIELD_STALENESS_TICKLER_CLI_ALIAS
290
+ ? FIELD_STALENESS_TICKLER
291
+ : name === FIELD_RUNTIME_AUTHORITY_CLI_ALIAS
292
+ ? FIELD_RUNTIME_AUTHORITY
293
+ : name;
280
294
  for (const field of inspectAllPolicies(projectRoot)) {
281
295
  if (field.name === normalized)
282
296
  return field;
@@ -0,0 +1,45 @@
1
+ /** Canonical registered policy field name (#2693). */
2
+ export declare const FIELD_PRODUCT_SIGNAL = "plan.policy.productSignal";
3
+ /** Short alias for `policy:show --field=productSignal`. */
4
+ export declare const FIELD_PRODUCT_SIGNAL_CLI_ALIAS = "productSignal";
5
+ export declare const DEFAULT_PRODUCT_SIGNAL_ENABLED = false;
6
+ /** Baked-in default private sink (#2693 D6). */
7
+ export declare const DEFAULT_PRODUCT_SIGNAL_SINK_REPO = "deftai/product-signal";
8
+ export interface ProductSignalConfig {
9
+ readonly enabled: boolean;
10
+ readonly sinkRepo: string;
11
+ }
12
+ export type ProductSignalSource = "typed" | "default" | "default-on-error";
13
+ export interface ProductSignalResolved extends ProductSignalConfig {
14
+ readonly source: ProductSignalSource;
15
+ readonly error: string | null;
16
+ }
17
+ export declare const PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE: string;
18
+ /** Validate a `plan.policy.productSignal` payload. */
19
+ export declare function validateProductSignal(value: unknown): string[];
20
+ /** Resolve `plan.policy.productSignal` from PROJECT-DEFINITION (#2693). */
21
+ export declare function resolveProductSignal(projectRoot: string): ProductSignalResolved;
22
+ /** Human-readable status line for CLI surfaces. */
23
+ export declare function formatProductSignalStatusLine(policy: ProductSignalResolved): string;
24
+ export interface ProductSignalPolicyField {
25
+ readonly name: typeof FIELD_PRODUCT_SIGNAL;
26
+ readonly current: ProductSignalConfig;
27
+ readonly default: ProductSignalConfig;
28
+ readonly source: string;
29
+ }
30
+ /** Inspector row for `policy:show --field=productSignal`. */
31
+ export declare function inspectProductSignal(data: Record<string, unknown> | null, projectRoot?: string): ProductSignalPolicyField;
32
+ export interface EnableProductSignalOptions {
33
+ readonly confirm: boolean;
34
+ readonly actor?: string;
35
+ readonly note?: string;
36
+ readonly sinkRepo?: string;
37
+ }
38
+ export interface EnableProductSignalResult {
39
+ readonly exitCode: 0 | 1 | 2;
40
+ readonly stdout: string;
41
+ readonly changed: boolean;
42
+ }
43
+ /** Persist `productSignal.enabled=true` after capability-cost disclosure (#2693). */
44
+ export declare function enableProductSignal(projectRoot: string, options: EnableProductSignalOptions): EnableProductSignalResult;
45
+ //# sourceMappingURL=product-signal.d.ts.map
@@ -0,0 +1,210 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
3
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
4
+ import { policyColonInvocation } from "./policy-invocation.js";
5
+ import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
6
+ /** Canonical registered policy field name (#2693). */
7
+ export const FIELD_PRODUCT_SIGNAL = "plan.policy.productSignal";
8
+ /** Short alias for `policy:show --field=productSignal`. */
9
+ export const FIELD_PRODUCT_SIGNAL_CLI_ALIAS = "productSignal";
10
+ export const DEFAULT_PRODUCT_SIGNAL_ENABLED = false;
11
+ /** Baked-in default private sink (#2693 D6). */
12
+ export const DEFAULT_PRODUCT_SIGNAL_SINK_REPO = "deftai/product-signal";
13
+ export const PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost disclosure -- enabling product signal opts into " +
14
+ "consented qualitative check-ins that may submit minimized local summaries " +
15
+ "to a private GitHub sink when you also grant install-level consent.\n" +
16
+ " \u2022 Default OFF; no ambient consent nag while disabled.\n" +
17
+ " \u2022 Outbound requires enable + recorded consent (`product-signal:consent`).\n" +
18
+ " \u2022 Payloads include install context and windowed value/health/helped summaries " +
19
+ "(not raw ledgers or chat).\n" +
20
+ " \u2022 Inspect: `" +
21
+ policyColonInvocation("show", " --field=productSignal") +
22
+ "` and `task product-signal:status`.\n" +
23
+ " \u2022 Reversible: set `enabled: false` or revoke consent.\n" +
24
+ " \u2022 Changes are recorded to meta/policy-changes.log for auditability.";
25
+ function defaultResolved(source, error = null) {
26
+ return {
27
+ enabled: DEFAULT_PRODUCT_SIGNAL_ENABLED,
28
+ sinkRepo: DEFAULT_PRODUCT_SIGNAL_SINK_REPO,
29
+ source,
30
+ error,
31
+ };
32
+ }
33
+ /** Validate a `plan.policy.productSignal` payload. */
34
+ export function validateProductSignal(value) {
35
+ if (value === null || value === undefined) {
36
+ return [];
37
+ }
38
+ if (typeof value !== "object" || Array.isArray(value)) {
39
+ return [`${FIELD_PRODUCT_SIGNAL} must be an object; got ${typeof value}`];
40
+ }
41
+ const rec = value;
42
+ const errors = [];
43
+ if ("enabled" in rec && typeof rec.enabled !== "boolean") {
44
+ errors.push(`${FIELD_PRODUCT_SIGNAL}.enabled must be a boolean`);
45
+ }
46
+ if ("sinkRepo" in rec && typeof rec.sinkRepo !== "string") {
47
+ errors.push(`${FIELD_PRODUCT_SIGNAL}.sinkRepo must be a string`);
48
+ }
49
+ return errors;
50
+ }
51
+ function resolveFromPolicyBlock(raw) {
52
+ const errors = validateProductSignal(raw);
53
+ if (errors.length > 0) {
54
+ return defaultResolved("default-on-error", errors[0] ?? "invalid productSignal block");
55
+ }
56
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
57
+ return defaultResolved("default");
58
+ }
59
+ const block = raw;
60
+ const enabled = typeof block.enabled === "boolean" ? block.enabled : DEFAULT_PRODUCT_SIGNAL_ENABLED;
61
+ const sinkRepo = typeof block.sinkRepo === "string" && block.sinkRepo.trim().length > 0
62
+ ? block.sinkRepo.trim()
63
+ : DEFAULT_PRODUCT_SIGNAL_SINK_REPO;
64
+ return {
65
+ enabled,
66
+ sinkRepo,
67
+ source: "typed",
68
+ error: null,
69
+ };
70
+ }
71
+ /** Resolve `plan.policy.productSignal` from PROJECT-DEFINITION (#2693). */
72
+ export function resolveProductSignal(projectRoot) {
73
+ const [data, err] = loadProjectDefinition(projectRoot);
74
+ if (data === null) {
75
+ return defaultResolved("default-on-error", err);
76
+ }
77
+ const policyBlock = readPlanPolicy(data.plan);
78
+ if (typeof policyBlock !== "object" ||
79
+ policyBlock === null ||
80
+ Array.isArray(policyBlock) ||
81
+ !("productSignal" in policyBlock)) {
82
+ return defaultResolved("default");
83
+ }
84
+ return resolveFromPolicyBlock(policyBlock.productSignal);
85
+ }
86
+ /** Human-readable status line for CLI surfaces. */
87
+ export function formatProductSignalStatusLine(policy) {
88
+ return (`[deft policy] productSignal enabled=${String(policy.enabled)} ` +
89
+ `sinkRepo=${policy.sinkRepo}.`);
90
+ }
91
+ function fieldFromResolved(resolved) {
92
+ return {
93
+ name: FIELD_PRODUCT_SIGNAL,
94
+ current: {
95
+ enabled: resolved.enabled,
96
+ sinkRepo: resolved.sinkRepo,
97
+ },
98
+ default: {
99
+ enabled: DEFAULT_PRODUCT_SIGNAL_ENABLED,
100
+ sinkRepo: DEFAULT_PRODUCT_SIGNAL_SINK_REPO,
101
+ },
102
+ source: resolved.source,
103
+ };
104
+ }
105
+ /** Inspector row for `policy:show --field=productSignal`. */
106
+ export function inspectProductSignal(data, projectRoot) {
107
+ if (data === null) {
108
+ return fieldFromResolved(defaultResolved("default"));
109
+ }
110
+ const policyBlock = readPlanPolicy(data.plan);
111
+ if (typeof policyBlock !== "object" ||
112
+ policyBlock === null ||
113
+ Array.isArray(policyBlock) ||
114
+ !("productSignal" in policyBlock)) {
115
+ if (projectRoot !== undefined) {
116
+ return fieldFromResolved(resolveProductSignal(projectRoot));
117
+ }
118
+ return fieldFromResolved(defaultResolved("default"));
119
+ }
120
+ return fieldFromResolved(resolveFromPolicyBlock(policyBlock.productSignal));
121
+ }
122
+ /** Persist `productSignal.enabled=true` after capability-cost disclosure (#2693). */
123
+ export function enableProductSignal(projectRoot, options) {
124
+ if (!options.confirm) {
125
+ return {
126
+ exitCode: 1,
127
+ stdout: `${PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE}\n\n` +
128
+ `Re-run with --confirm to apply: task product-signal:enable -- --confirm\n`,
129
+ changed: false,
130
+ };
131
+ }
132
+ const path = projectDefinitionPath(projectRoot);
133
+ try {
134
+ const { changed } = projectDefinitionMutationLock(projectRoot, () => {
135
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
136
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
137
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
138
+ }
139
+ const data = parsed;
140
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
141
+ if (data.plan === undefined) {
142
+ data.plan = {};
143
+ }
144
+ else {
145
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
146
+ }
147
+ }
148
+ const plan = data.plan;
149
+ migrateLegacyPolicyKey(plan);
150
+ const existingPolicy = plan[PLAN_POLICY_KEY];
151
+ if (typeof existingPolicy !== "object" ||
152
+ existingPolicy === null ||
153
+ Array.isArray(existingPolicy)) {
154
+ if (existingPolicy === undefined) {
155
+ plan[PLAN_POLICY_KEY] = {};
156
+ }
157
+ else {
158
+ throw new Error("plan.policy is not an object");
159
+ }
160
+ }
161
+ const policyBlock = plan[PLAN_POLICY_KEY];
162
+ const previous = policyBlock.productSignal;
163
+ const prevObj = typeof previous === "object" && previous !== null && !Array.isArray(previous)
164
+ ? previous
165
+ : {};
166
+ const sinkRepo = options.sinkRepo?.trim() ||
167
+ (typeof prevObj.sinkRepo === "string" && prevObj.sinkRepo.trim().length > 0
168
+ ? prevObj.sinkRepo.trim()
169
+ : DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
170
+ const nextBlock = { enabled: true, sinkRepo };
171
+ const previousNormalized = resolveFromPolicyBlock(previous);
172
+ const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
173
+ previousNormalized.sinkRepo !== nextBlock.sinkRepo;
174
+ policyBlock.productSignal = nextBlock;
175
+ if (changedFlag) {
176
+ atomicWriteProjectDefinition(path, data);
177
+ }
178
+ const actor = options.actor ?? "task product-signal:enable";
179
+ const note = options.note ?? "";
180
+ const parts = [
181
+ `actor=${actor}`,
182
+ "productSignal.enabled=true",
183
+ `sinkRepo=${sinkRepo}`,
184
+ `previous=${JSON.stringify(previous ?? null)}`,
185
+ ];
186
+ if (note) {
187
+ parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
188
+ }
189
+ appendAuditLog(projectRoot, parts.join(" "));
190
+ return { changed: changedFlag };
191
+ });
192
+ const resolved = resolveProductSignal(projectRoot);
193
+ const lines = [
194
+ `\u2713 ${FIELD_PRODUCT_SIGNAL}.enabled=true (product-signal ON).`,
195
+ changed
196
+ ? " audit: meta/policy-changes.log updated."
197
+ : " no-op: value already matched (audit entry still appended for trail).",
198
+ formatProductSignalStatusLine(resolved),
199
+ ];
200
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
201
+ }
202
+ catch (err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ if (message.includes("PROJECT-DEFINITION not found")) {
205
+ return { exitCode: 2, stdout: `\u274c ${message}\n`, changed: false };
206
+ }
207
+ return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
208
+ }
209
+ }
210
+ //# sourceMappingURL=product-signal.js.map
@@ -0,0 +1,18 @@
1
+ import { type RunGhApiFn } from "../scm/gh-rest.js";
2
+ export type ActorNameSource = "user-md" | "gh-login" | "unnamed";
3
+ export interface ResolvedActorName {
4
+ readonly actorName: string;
5
+ readonly actorNameSource: ActorNameSource;
6
+ readonly displayName: string;
7
+ }
8
+ /** Normalize for thread-key matching (#2693 D8). */
9
+ export declare function normalizeActorKey(name: string): string;
10
+ /** Parse addressing-name from USER.md Personal section (#2693 D8). */
11
+ export declare function parseAddressingNameFromUserMd(text: string): string | null;
12
+ export interface ResolveActorNameOptions {
13
+ readonly projectRoot?: string;
14
+ readonly runGhApiFn?: RunGhApiFn;
15
+ }
16
+ /** Resolve actorName with USER.md -> gh-login -> unnamed precedence (#2693 D8). */
17
+ export declare function resolveActorName(options?: ResolveActorNameOptions): ResolvedActorName;
18
+ //# sourceMappingURL=actor-name.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { runGhApi } from "../scm/gh-rest.js";
3
+ import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
4
+ /** Normalize for thread-key matching (#2693 D8). */
5
+ export function normalizeActorKey(name) {
6
+ return name.trim().replace(/\s+/g, " ").toLowerCase();
7
+ }
8
+ /** Parse addressing-name from USER.md Personal section (#2693 D8). */
9
+ export function parseAddressingNameFromUserMd(text) {
10
+ const lines = text.split(/\r?\n/);
11
+ let inPersonal = false;
12
+ for (const line of lines) {
13
+ const trimmed = line.trim();
14
+ if (/^##\s+personal\b/i.test(trimmed)) {
15
+ inPersonal = true;
16
+ continue;
17
+ }
18
+ if (inPersonal && /^##\s+/.test(trimmed)) {
19
+ break;
20
+ }
21
+ const nameMatch = trimmed.match(/^(?:-\s*)?(?:\*\*)?Name(?:\*\*)?\s*:\s*(.+)$/i) ??
22
+ trimmed.match(/^(?:-\s*)?(?:\*\*)?addressing-name(?:\*\*)?\s*:\s*(.+)$/i);
23
+ if (nameMatch?.[1] !== undefined) {
24
+ const value = nameMatch[1].replace(/\*\*/g, "").trim();
25
+ if (value.length > 0) {
26
+ return value;
27
+ }
28
+ }
29
+ if (!inPersonal) {
30
+ const topMatch = trimmed.match(/^(?:\*\*)?Name(?:\*\*)?\s*:\s*(.+)$/i);
31
+ if (topMatch?.[1] !== undefined) {
32
+ const value = topMatch[1].replace(/\*\*/g, "").trim();
33
+ if (value.length > 0) {
34
+ return value;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ function readGhLogin(runGhApiFn) {
42
+ try {
43
+ const result = (runGhApiFn ?? runGhApi)(["user"], { timeout: 15 });
44
+ if (result.returncode !== 0) {
45
+ return null;
46
+ }
47
+ const parsed = JSON.parse(result.stdout.trim());
48
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
49
+ const login = parsed.login;
50
+ if (typeof login === "string" && login.trim().length > 0) {
51
+ return login.trim();
52
+ }
53
+ }
54
+ }
55
+ catch {
56
+ // best-effort
57
+ }
58
+ return null;
59
+ }
60
+ /** Resolve actorName with USER.md -> gh-login -> unnamed precedence (#2693 D8). */
61
+ export function resolveActorName(options = {}) {
62
+ const projectRoot = options.projectRoot ?? process.cwd();
63
+ const userMd = resolveUserMdPath({ projectRoot });
64
+ if (userMd.found) {
65
+ try {
66
+ const text = readFileSync(userMd.path, "utf8");
67
+ const fromUser = parseAddressingNameFromUserMd(text);
68
+ if (fromUser !== null) {
69
+ return {
70
+ actorName: fromUser,
71
+ actorNameSource: "user-md",
72
+ displayName: fromUser,
73
+ };
74
+ }
75
+ }
76
+ catch {
77
+ // fall through
78
+ }
79
+ }
80
+ const ghLogin = readGhLogin(options.runGhApiFn);
81
+ if (ghLogin !== null) {
82
+ return {
83
+ actorName: ghLogin,
84
+ actorNameSource: "gh-login",
85
+ displayName: ghLogin,
86
+ };
87
+ }
88
+ return {
89
+ actorName: "unnamed",
90
+ actorNameSource: "unnamed",
91
+ displayName: "unnamed",
92
+ };
93
+ }
94
+ //# sourceMappingURL=actor-name.js.map
@@ -0,0 +1,30 @@
1
+ export declare const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
2
+ /** Consent record schema version (#2693 D2). */
3
+ export declare const PRODUCT_SIGNAL_CONSENT_VERSION = 1;
4
+ /** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
5
+ export declare const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
6
+ export interface ProductSignalConsentRecord {
7
+ readonly consentVersion: number;
8
+ readonly grantedAt: string;
9
+ readonly tier: string;
10
+ readonly revokedAt?: string;
11
+ }
12
+ export interface ResolveConsentPathOptions {
13
+ readonly platform?: NodeJS.Platform;
14
+ readonly env?: NodeJS.ProcessEnv;
15
+ readonly homeDir?: string;
16
+ }
17
+ /** Platform-config consent path adjacent to USER.md (#2693 D2). */
18
+ export declare function resolveProductSignalConsentPath(options?: ResolveConsentPathOptions): string;
19
+ /** Read consent file; returns null when absent, invalid, or revoked. */
20
+ export declare function readProductSignalConsent(options?: ResolveConsentPathOptions): ProductSignalConsentRecord | null;
21
+ /** True when a non-revoked consent grant exists. */
22
+ export declare function isProductSignalConsented(options?: ResolveConsentPathOptions): boolean;
23
+ export interface WriteConsentOptions extends ResolveConsentPathOptions {
24
+ readonly now?: Date;
25
+ }
26
+ /** Write a fresh consent grant (#2693 D17 yes path). */
27
+ export declare function grantProductSignalConsent(options?: WriteConsentOptions): ProductSignalConsentRecord;
28
+ /** Revoke consent by setting revokedAt (#2693 D2). */
29
+ export declare function revokeProductSignalConsent(options?: WriteConsentOptions): boolean;
30
+ //# sourceMappingURL=consent.d.ts.map