@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
@@ -0,0 +1,290 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
4
+ import { resolveProjectRoot } from "../scope/project-context.js";
5
+ import { resolveActorName } from "./actor-name.js";
6
+ import { grantProductSignalConsent, isProductSignalConsented, readProductSignalConsent, revokeProductSignalConsent, } from "./consent.js";
7
+ import { evaluateProductSignalGates } from "./gates.js";
8
+ import { GitHubPrivateSinkAdapter } from "./github-private-sink-adapter.js";
9
+ import { collectInstallContext } from "./install-context.js";
10
+ import { assembleLocalSignalSummary } from "./local-signal-summary.js";
11
+ import { PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION, readSkillsSummarySidecar, validateProductSignalPayload, } from "./payload.js";
12
+ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
13
+ export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
14
+ function recordLastSubmit(projectRoot, outcome, url) {
15
+ const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
16
+ try {
17
+ mkdirSync(join(path, ".."), { recursive: true });
18
+ appendFileSync(path, `${JSON.stringify({
19
+ outcome,
20
+ issueUrl: url,
21
+ at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
22
+ })}\n`, "utf8");
23
+ }
24
+ catch {
25
+ // observability only
26
+ }
27
+ }
28
+ function readLastSubmitSummary(projectRoot) {
29
+ const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
30
+ if (!existsSync(path)) {
31
+ return null;
32
+ }
33
+ try {
34
+ const lines = readFileSync(path, "utf8")
35
+ .split("\n")
36
+ .filter((l) => l.trim());
37
+ const last = lines[lines.length - 1];
38
+ if (!last) {
39
+ return null;
40
+ }
41
+ const rec = JSON.parse(last);
42
+ if (rec !== null && typeof rec === "object" && !Array.isArray(rec)) {
43
+ const at = rec.at;
44
+ const outcome = rec.outcome;
45
+ const url = rec.issueUrl;
46
+ return `last=${String(outcome)} at=${String(at)} url=${String(url ?? "none")}`;
47
+ }
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ return null;
53
+ }
54
+ /** Assemble a versioned product-signal payload (#2693 D7). */
55
+ export function assembleProductSignalPayload(projectRoot, input) {
56
+ const ctx = collectInstallContext(projectRoot);
57
+ const actor = resolveActorName({ projectRoot });
58
+ const consent = readProductSignalConsent();
59
+ const human = {
60
+ nps: input.human?.nps ?? null,
61
+ answers: input.human?.answers ?? [],
62
+ freeText: input.human?.freeText ?? null,
63
+ };
64
+ return {
65
+ schemaVersion: PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION,
66
+ surface: input.surface,
67
+ installId: ctx.installId,
68
+ actorName: actor.displayName,
69
+ actorNameSource: actor.actorNameSource,
70
+ directiveVersion: ctx.directiveVersion,
71
+ os: ctx.os,
72
+ osVersion: ctx.osVersion,
73
+ shell: ctx.shell,
74
+ harness: ctx.harness,
75
+ harnessVersion: ctx.harnessVersion,
76
+ consentTier: consent?.tier ?? "none",
77
+ consentSource: "user",
78
+ consentVersion: consent ? String(consent.consentVersion) : "0",
79
+ human,
80
+ agentNotes: input.agentNotes ?? null,
81
+ localSignalSummary: assembleLocalSignalSummary(projectRoot),
82
+ skillsSummary: readSkillsSummarySidecar(projectRoot),
83
+ collectedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
84
+ };
85
+ }
86
+ /** Submit product-signal through gates + adapter (#2693). */
87
+ export async function submitProductSignal(options) {
88
+ const root = resolveProjectRoot(options.projectRoot ?? undefined);
89
+ if (root === null) {
90
+ return { outcome: "error-config", exitCode: 2, message: "project root not found\n" };
91
+ }
92
+ if (!options.skipGates) {
93
+ const gates = evaluateProductSignalGates({ projectRoot: root });
94
+ if (!gates.allowed) {
95
+ return { outcome: gates.outcome, exitCode: 0, message: `${gates.message}\n` };
96
+ }
97
+ }
98
+ const payload = assembleProductSignalPayload(root, options);
99
+ const validationErrors = validateProductSignalPayload(payload);
100
+ if (validationErrors.length > 0) {
101
+ return {
102
+ outcome: "validation",
103
+ exitCode: 1,
104
+ message: `validation failed: ${validationErrors.join("; ")}\n`,
105
+ payload,
106
+ };
107
+ }
108
+ if (options.dryRun) {
109
+ return {
110
+ outcome: "dry-run",
111
+ exitCode: 0,
112
+ message: `[dry-run] payload valid for ${payload.surface}\n`,
113
+ payload,
114
+ };
115
+ }
116
+ const policy = resolveProductSignal(root);
117
+ const adapter = new GitHubPrivateSinkAdapter({ sinkRepo: policy.sinkRepo });
118
+ const result = await adapter.submit(payload, { gapText: options.gapText });
119
+ if (result.outcome === "submitted") {
120
+ recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
121
+ }
122
+ const urlLine = result.issueUrl ? ` url=${result.issueUrl}` : "";
123
+ return {
124
+ outcome: result.outcome,
125
+ exitCode: 0,
126
+ message: `${result.message}${urlLine}\n`,
127
+ issueUrl: result.issueUrl,
128
+ payload,
129
+ };
130
+ }
131
+ export function runProductSignalStatus(projectRoot) {
132
+ const root = resolveProjectRoot(projectRoot ?? undefined) ?? process.cwd();
133
+ const policy = resolveProductSignal(root);
134
+ const consented = isProductSignalConsented();
135
+ const last = readLastSubmitSummary(root);
136
+ const lines = [
137
+ formatProductSignalStatusLine(policy),
138
+ `[deft product-signal] consented=${String(consented)}`,
139
+ last ? `[deft product-signal] ${last}` : "[deft product-signal] last submit: none",
140
+ ];
141
+ return { exitCode: 0, text: `${lines.join("\n")}\n` };
142
+ }
143
+ export function runProductSignalEnable(projectRoot, confirm) {
144
+ const root = resolveProjectRoot(projectRoot ?? undefined);
145
+ if (root === null) {
146
+ return { exitCode: 2, text: "project root not found\n" };
147
+ }
148
+ const result = enableProductSignal(root, { confirm });
149
+ return { exitCode: result.exitCode, text: result.stdout };
150
+ }
151
+ export function runProductSignalConsent(action) {
152
+ if (action === "grant") {
153
+ const record = grantProductSignalConsent();
154
+ return {
155
+ exitCode: 0,
156
+ text: `product-signal consent granted (tier=${record.tier}, version=${record.consentVersion}).\n`,
157
+ };
158
+ }
159
+ const ok = revokeProductSignalConsent();
160
+ if (!ok) {
161
+ return { exitCode: 1, text: "no consent file to revoke\n" };
162
+ }
163
+ return { exitCode: 0, text: "product-signal consent revoked\n" };
164
+ }
165
+ export function runProductSignalBootstrapSink(dryRun) {
166
+ const result = bootstrapProductSignalSink({ dryRun });
167
+ return { exitCode: result.exitCode, text: result.stdout };
168
+ }
169
+ export function parseProductSignalSubmitArgs(argv) {
170
+ let surface = "pulse";
171
+ let dryRun = false;
172
+ let json = false;
173
+ let projectRoot = ".";
174
+ let nps = null;
175
+ for (let i = 0; i < argv.length; i += 1) {
176
+ const arg = argv[i];
177
+ if (arg === "--surface") {
178
+ const v = argv[i + 1];
179
+ if (v !== "pulse" && v !== "portrait") {
180
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --surface" };
181
+ }
182
+ surface = v;
183
+ i += 1;
184
+ }
185
+ else if (arg?.startsWith("--surface=")) {
186
+ const v = arg.slice("--surface=".length);
187
+ if (v !== "pulse" && v !== "portrait") {
188
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --surface" };
189
+ }
190
+ surface = v;
191
+ }
192
+ else if (arg === "--dry-run") {
193
+ dryRun = true;
194
+ }
195
+ else if (arg === "--json") {
196
+ json = true;
197
+ }
198
+ else if (arg === "--nps") {
199
+ const v = Number(argv[i + 1]);
200
+ if (!Number.isInteger(v) || v < 0 || v > 10) {
201
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --nps" };
202
+ }
203
+ nps = v;
204
+ i += 1;
205
+ }
206
+ else if (arg === "--project-root") {
207
+ projectRoot = argv[i + 1] ?? ".";
208
+ i += 1;
209
+ }
210
+ else if (arg?.startsWith("--project-root=")) {
211
+ projectRoot = arg.slice("--project-root=".length);
212
+ }
213
+ else {
214
+ return { surface, dryRun, json, projectRoot, nps, error: `unrecognized argument: ${arg}` };
215
+ }
216
+ }
217
+ return { surface, dryRun, json, projectRoot, nps };
218
+ }
219
+ /** CLI module entrypoint for dispatch (#2693). */
220
+ export async function productSignalMain(argv = process.argv.slice(2)) {
221
+ const sub = argv[0];
222
+ if (sub === "status") {
223
+ const rootIdx = argv.indexOf("--project-root");
224
+ const root = rootIdx >= 0
225
+ ? (argv[rootIdx + 1] ?? ".")
226
+ : (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
227
+ const result = runProductSignalStatus(root);
228
+ process.stdout.write(result.text);
229
+ return result.exitCode;
230
+ }
231
+ if (sub === "enable") {
232
+ const confirm = argv.includes("--confirm");
233
+ const rootIdx = argv.indexOf("--project-root");
234
+ const root = rootIdx >= 0
235
+ ? (argv[rootIdx + 1] ?? ".")
236
+ : (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
237
+ const result = runProductSignalEnable(root, confirm);
238
+ process.stdout.write(result.text);
239
+ return result.exitCode;
240
+ }
241
+ if (sub === "consent") {
242
+ const grant = argv.includes("--grant");
243
+ const revoke = argv.includes("--revoke");
244
+ if (grant === revoke) {
245
+ process.stderr.write("usage: product-signal consent -- --grant|--revoke\n");
246
+ return 1;
247
+ }
248
+ const result = runProductSignalConsent(grant ? "grant" : "revoke");
249
+ process.stdout.write(result.text);
250
+ return result.exitCode;
251
+ }
252
+ if (sub === "bootstrap-sink") {
253
+ const result = runProductSignalBootstrapSink(argv.includes("--dry-run"));
254
+ process.stdout.write(result.text);
255
+ return result.exitCode;
256
+ }
257
+ if (sub === "submit") {
258
+ const parsed = parseProductSignalSubmitArgs(argv.slice(1));
259
+ if (parsed.error) {
260
+ process.stderr.write(`${parsed.error}\n`);
261
+ return 1;
262
+ }
263
+ const result = await submitProductSignal({
264
+ projectRoot: parsed.projectRoot,
265
+ surface: parsed.surface,
266
+ dryRun: parsed.dryRun,
267
+ json: parsed.json,
268
+ human: parsed.nps !== null ? { nps: parsed.nps, answers: [], freeText: null } : undefined,
269
+ });
270
+ if (parsed.json) {
271
+ process.stdout.write(`${JSON.stringify({
272
+ outcome: result.outcome,
273
+ exit_code: result.exitCode,
274
+ message: result.message.trim(),
275
+ issue_url: result.issueUrl ?? null,
276
+ payload: result.payload ?? null,
277
+ }, null, 2)}\n`);
278
+ }
279
+ else {
280
+ process.stdout.write(result.message);
281
+ }
282
+ return result.exitCode;
283
+ }
284
+ process.stderr.write("usage: product-signal [status|enable|consent|submit|bootstrap-sink] ...\n");
285
+ return 1;
286
+ }
287
+ export async function mainEntry(argv = process.argv.slice(2)) {
288
+ return productSignalMain(argv);
289
+ }
290
+ //# sourceMappingURL=submit.js.map
@@ -53,9 +53,11 @@ export interface RestIssueListOptions {
53
53
  export declare function restIssueList(repo: string, options?: RestIssueListOptions, seams?: GhRestSeams): Record<string, unknown>[];
54
54
  export declare const REST_MAX_PER_PAGE = 100;
55
55
  export declare const REST_PAGINATION_MAX_PAGES = 100;
56
- export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
56
+ export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
57
57
  export declare function restCreateIssue(repo: string, title: string, body: string, labels?: readonly string[], seams?: GhRestSeams): Record<string, unknown>;
58
58
  export declare function restPostComment(repo: string, n: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
59
+ export declare function restUpdateIssue(repo: string, n: number, patch: Record<string, unknown>, seams?: GhRestSeams): Record<string, unknown>;
60
+ export declare function restCreateLabel(repo: string, name: string, color: string, description: string, seams?: GhRestSeams): Record<string, unknown>;
59
61
  export declare function restCloseIssue(repo: string, n: number, reason?: string | null, seams?: GhRestSeams): Record<string, unknown>;
60
62
  export declare function restOpenPr(repo: string, head: string, base: string, title: string, body: string, options?: {
61
63
  draft?: boolean;
@@ -171,6 +171,8 @@ export const REST_PAGINATION_MAX_PAGES = 100;
171
171
  export const PUBLIC_HELPERS = [
172
172
  "restCreateIssue",
173
173
  "restPostComment",
174
+ "restUpdateIssue",
175
+ "restCreateLabel",
174
176
  "restCloseIssue",
175
177
  "restOpenPr",
176
178
  "restMergePr",
@@ -224,6 +226,26 @@ export function restPostComment(repo, n, body, seams = {}) {
224
226
  ...seams,
225
227
  });
226
228
  }
229
+ export function restUpdateIssue(repo, n, patch, seams = {}) {
230
+ const [owner, name] = splitRepo(repo);
231
+ const endpoint = `repos/${owner}/${name}/issues/${n}`;
232
+ return execMutation([endpoint, "--method", "PATCH"], {
233
+ endpoint,
234
+ payload: patch,
235
+ hint: "verify repo permissions and issue number; check gh auth status",
236
+ ...seams,
237
+ });
238
+ }
239
+ export function restCreateLabel(repo, name, color, description, seams = {}) {
240
+ const [owner, repoName] = splitRepo(repo);
241
+ const endpoint = `repos/${owner}/${repoName}/labels`;
242
+ return execMutation([endpoint, "--method", "POST"], {
243
+ endpoint,
244
+ payload: { name, color, description },
245
+ hint: "verify repo permissions; label may already exist (422 is acceptable for idempotent bootstrap)",
246
+ ...seams,
247
+ });
248
+ }
227
249
  export function restCloseIssue(repo, n, reason = "completed", seams = {}) {
228
250
  const [owner, name] = splitRepo(repo);
229
251
  const endpoint = `repos/${owner}/${name}/issues/${n}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.80.0",
3
+ "version": "0.81.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -309,8 +309,8 @@
309
309
  "provenance": true
310
310
  },
311
311
  "dependencies": {
312
- "@deftai/directive-content": "^0.80.0",
313
- "@deftai/directive-types": "^0.80.0",
312
+ "@deftai/directive-content": "^0.81.0",
313
+ "@deftai/directive-types": "^0.81.0",
314
314
  "archiver": "^8.0.0"
315
315
  },
316
316
  "scripts": {