@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
@@ -0,0 +1,309 @@
1
+ import { createHash } from "node:crypto";
2
+ import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { readCorePackageVersion } from "../engine-version.js";
6
+ import { resolveEvalPath } from "../layout/resolve.js";
7
+ import { BYTE_DIFF_WHOLE_FILE_THRESHOLD, InstrumentedVbriefCrud } from "./crud-telemetry.js";
8
+ import { evaluateHealth } from "./health.js";
9
+ export const GOLDEN_RUNS_HISTORY_REL = "results/golden-runs.jsonl";
10
+ export const GOLDEN_RUN_SCHEMA_VERSION = 1;
11
+ export const GOLDEN_CORPUS_VERSION = "2026-07-05-tier2-fixture-v1";
12
+ const VALID_VBRIEF = `{
13
+ "vBRIEFInfo": { "version": "0.6", "description": "golden fixture" },
14
+ "plan": {
15
+ "id": "golden-fixture",
16
+ "title": "Golden fixture",
17
+ "status": "pending",
18
+ "narratives": { "Description": "Valid golden corpus document." },
19
+ "items": [
20
+ {
21
+ "id": "golden-a1",
22
+ "title": "Item",
23
+ "status": "pending",
24
+ "narrative": { "Acceptance": "Schema valid." }
25
+ }
26
+ ]
27
+ }
28
+ }`;
29
+ const INVENTED_KEY_VBRIEF = VALID_VBRIEF.replace('"items": [', '"agentInventedField": "bad",\n "items": [');
30
+ function passRate(results, holdout) {
31
+ const subset = results.filter((r) => r.holdout === holdout);
32
+ if (subset.length === 0) {
33
+ return 0;
34
+ }
35
+ return subset.filter((r) => r.passed).length / subset.length;
36
+ }
37
+ function summarizeResults(results) {
38
+ const primary = results.filter((r) => !r.holdout);
39
+ const holdout = results.filter((r) => r.holdout);
40
+ const primaryPassRate = passRate(results, false);
41
+ const holdoutPassRate = passRate(results, true);
42
+ return {
43
+ primaryPassRate,
44
+ holdoutPassRate,
45
+ passRate: results.length === 0 ? 0 : results.filter((r) => r.passed).length / results.length,
46
+ primaryTotal: primary.length,
47
+ holdoutTotal: holdout.length,
48
+ };
49
+ }
50
+ function gradeCrudValidCreate(context) {
51
+ const path = join(context.tempDir, "create.json");
52
+ const crud = new InstrumentedVbriefCrud({ directiveVersion: context.directiveVersion });
53
+ const result = crud.create(path, VALID_VBRIEF);
54
+ const metric = crud.getMetrics()[0];
55
+ return {
56
+ passed: result.ok && metric?.schemaValid === true,
57
+ metrics: {
58
+ schemaValid: metric?.schemaValid === true,
59
+ fieldInventionCount: metric?.fieldInventionCount ?? -1,
60
+ },
61
+ };
62
+ }
63
+ function gradeCrudRejectInvention(context) {
64
+ const path = join(context.tempDir, "invented.json");
65
+ const crud = new InstrumentedVbriefCrud({ directiveVersion: context.directiveVersion });
66
+ crud.create(path, INVENTED_KEY_VBRIEF);
67
+ const metric = crud.getMetrics()[0];
68
+ const inventedKeys = metric?.inventedKeys.length ?? 0;
69
+ return {
70
+ passed: metric?.schemaValid === true && inventedKeys > 0 && (metric?.fieldInventionCount ?? 0) > 0,
71
+ metrics: {
72
+ detectedInvention: inventedKeys > 0,
73
+ fieldInventionCount: metric?.fieldInventionCount ?? -1,
74
+ },
75
+ };
76
+ }
77
+ function gradeCrudSurgicalUpdate(context) {
78
+ const path = join(context.tempDir, "update.json");
79
+ const crud = new InstrumentedVbriefCrud({ directiveVersion: context.directiveVersion });
80
+ crud.create(path, VALID_VBRIEF);
81
+ const updated = VALID_VBRIEF.replace('"pending"', '"running"');
82
+ crud.update(path, updated);
83
+ const updateMetric = crud.getMetrics().find((m) => m.operation === "update");
84
+ return {
85
+ passed: updateMetric?.schemaValid === true &&
86
+ updateMetric.byteDiffMinimality === "surgical" &&
87
+ (updateMetric.byteDiffChangedRatio ?? 1) < BYTE_DIFF_WHOLE_FILE_THRESHOLD,
88
+ metrics: {
89
+ schemaValid: updateMetric?.schemaValid === true,
90
+ byteDiffMinimalitySurgical: updateMetric?.byteDiffMinimality === "surgical",
91
+ changedRatio: updateMetric?.byteDiffChangedRatio ?? -1,
92
+ },
93
+ };
94
+ }
95
+ function seedHealthFixture(tempDir) {
96
+ writeFileSync(join(tempDir, "xbrief", "PROJECT-DEFINITION.xbrief.json"), JSON.stringify({
97
+ xBRIEFInfo: { version: "0.6" },
98
+ plan: {
99
+ title: "Golden health fixture",
100
+ status: "running",
101
+ items: [],
102
+ "x-directive/policy": { triageScope: [{ rule: "all-open" }] },
103
+ },
104
+ }), "utf8");
105
+ writeFileSync(join(tempDir, "AGENTS.md"), "<!-- deft:managed-section v3 -->\n<!-- /deft:managed-section -->\n", "utf8");
106
+ }
107
+ function gradeHealthFixture(context) {
108
+ seedHealthFixture(context.tempDir);
109
+ const health = evaluateHealth({
110
+ projectRoot: context.tempDir,
111
+ persist: false,
112
+ frameworkSource: false,
113
+ });
114
+ return {
115
+ passed: health.report !== null && typeof health.report.score === "number",
116
+ metrics: {
117
+ score: health.report?.score ?? -1,
118
+ gateCount: health.report?.gates.length ?? -1,
119
+ },
120
+ };
121
+ }
122
+ function gradeHoldoutSchemaRoundtrip(context) {
123
+ const path = join(context.tempDir, "roundtrip.json");
124
+ const crud = new InstrumentedVbriefCrud({ directiveVersion: context.directiveVersion });
125
+ crud.create(path, VALID_VBRIEF);
126
+ const read = crud.read(path);
127
+ const roundtripOk = read.ok && read.content === VALID_VBRIEF;
128
+ const noise = (context.seed % 997) / 997;
129
+ return {
130
+ passed: roundtripOk && noise < 0.99,
131
+ metrics: { roundtripOk, seedNoise: noise },
132
+ };
133
+ }
134
+ function gradeHoldoutByteDiffTripwire(context) {
135
+ const path = join(context.tempDir, "rewrite.json");
136
+ const crud = new InstrumentedVbriefCrud({ directiveVersion: context.directiveVersion });
137
+ crud.create(path, VALID_VBRIEF);
138
+ const rewritten = JSON.stringify(JSON.parse(VALID_VBRIEF), null, 2);
139
+ crud.update(path, rewritten);
140
+ const updateMetric = crud.getMetrics().find((m) => m.operation === "update");
141
+ const detectedRewrite = updateMetric?.byteDiffMinimality === "whole-file-rewrite";
142
+ const tripwire = (context.seed % 991) / 991;
143
+ return {
144
+ passed: detectedRewrite && tripwire < 0.99,
145
+ metrics: {
146
+ detectedRewrite,
147
+ tripwire,
148
+ },
149
+ };
150
+ }
151
+ /** Fixed golden corpus with objective graders (#1703 Tier 2). */
152
+ export const GOLDEN_CORPUS = [
153
+ {
154
+ id: "crud-valid-create",
155
+ title: "Instrumented create accepts valid vBRIEF",
156
+ holdout: false,
157
+ grade: gradeCrudValidCreate,
158
+ },
159
+ {
160
+ id: "crud-reject-invention",
161
+ title: "Instrumented create rejects invented keys",
162
+ holdout: false,
163
+ grade: gradeCrudRejectInvention,
164
+ },
165
+ {
166
+ id: "crud-surgical-update",
167
+ title: "Instrumented update classifies surgical edits",
168
+ holdout: false,
169
+ grade: gradeCrudSurgicalUpdate,
170
+ },
171
+ {
172
+ id: "health-fixture-score",
173
+ title: "Tier-0 health eval runs on fixture repo",
174
+ holdout: false,
175
+ grade: gradeHealthFixture,
176
+ },
177
+ {
178
+ id: "holdout-schema-roundtrip",
179
+ title: "Holdout: read-after-create roundtrip",
180
+ holdout: true,
181
+ grade: gradeHoldoutSchemaRoundtrip,
182
+ },
183
+ {
184
+ id: "holdout-byte-diff-tripwire",
185
+ title: "Holdout: whole-file rewrite detection tripwire",
186
+ holdout: true,
187
+ grade: gradeHoldoutByteDiffTripwire,
188
+ },
189
+ ];
190
+ /** Absolute path to the golden-run results ledger. */
191
+ export function goldenRunsHistoryPath(projectRoot) {
192
+ return resolveEvalPath(projectRoot, GOLDEN_RUNS_HISTORY_REL);
193
+ }
194
+ /** Append one golden run to the versioned ledger (#1703 Tier 2). */
195
+ export function persistGoldenRun(projectRoot, record) {
196
+ const path = goldenRunsHistoryPath(projectRoot);
197
+ mkdirSync(dirname(path), { recursive: true });
198
+ appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
199
+ }
200
+ /** Stable hash for rotating holdout selection (#1703 Goodhart mitigation). */
201
+ export function holdoutRotationIndex(directiveVersion, model, holdoutCount) {
202
+ if (holdoutCount <= 0) {
203
+ return 0;
204
+ }
205
+ const digest = createHash("sha256")
206
+ .update(`${directiveVersion}\0${model}\0${GOLDEN_CORPUS_VERSION}`)
207
+ .digest("hex");
208
+ return Number.parseInt(digest.slice(0, 8), 16) % holdoutCount;
209
+ }
210
+ /** Select the active holdout task for this version × model tuple. */
211
+ export function selectRotatingHoldoutTask(directiveVersion, model) {
212
+ const holdouts = GOLDEN_CORPUS.filter((task) => task.holdout);
213
+ if (holdouts.length === 0) {
214
+ return null;
215
+ }
216
+ return holdouts[holdoutRotationIndex(directiveVersion, model, holdouts.length)] ?? null;
217
+ }
218
+ function seedTempDir(taskId, seed) {
219
+ const base = mkdtempSync(join(tmpdir(), `deft-golden-${taskId}-seed-${seed}-`));
220
+ mkdirSync(join(base, "xbrief"), { recursive: true });
221
+ return base;
222
+ }
223
+ function runIdFor(directiveVersion, model, harness, seeds) {
224
+ const digest = createHash("sha256")
225
+ .update(`${directiveVersion}\0${model}\0${harness}\0${seeds.join(",")}\0${GOLDEN_CORPUS_VERSION}`)
226
+ .digest("hex");
227
+ return digest.slice(0, 12);
228
+ }
229
+ /** Execute the golden corpus for one model × seed set and optionally persist (#1703 Tier 2). */
230
+ export function runGoldenEval(options) {
231
+ if (!options.model.trim()) {
232
+ return { code: 2, record: null, message: "eval:run: --model is required" };
233
+ }
234
+ const projectRoot = options.projectRoot ?? process.cwd();
235
+ const directiveVersion = options.directiveVersion ?? readCorePackageVersion();
236
+ const harness = options.harness ?? "deterministic-fixture";
237
+ const seeds = options.seeds ?? [1, 2, 3];
238
+ const now = options.now ?? (() => new Date());
239
+ const persist = options.persist ?? true;
240
+ if (seeds.length === 0) {
241
+ return { code: 2, record: null, message: "eval:run: at least one seed is required" };
242
+ }
243
+ const primaryTasks = GOLDEN_CORPUS.filter((task) => !task.holdout);
244
+ const rotatingHoldout = selectRotatingHoldoutTask(directiveVersion, options.model);
245
+ const tasksToRun = [...primaryTasks];
246
+ if (rotatingHoldout !== null) {
247
+ tasksToRun.push(rotatingHoldout);
248
+ }
249
+ const results = [];
250
+ const scratchDirs = [];
251
+ for (const task of tasksToRun) {
252
+ for (const seed of seeds) {
253
+ const tempDir = seedTempDir(task.id, seed);
254
+ scratchDirs.push(tempDir);
255
+ const grade = task.grade({
256
+ tempDir,
257
+ seed,
258
+ directiveVersion,
259
+ model: options.model,
260
+ });
261
+ results.push({
262
+ taskId: task.id,
263
+ seed,
264
+ passed: grade.passed,
265
+ holdout: task.holdout,
266
+ metrics: grade.metrics,
267
+ });
268
+ }
269
+ }
270
+ for (const dir of scratchDirs) {
271
+ rmSync(dir, { recursive: true, force: true });
272
+ }
273
+ const summary = summarizeResults(results);
274
+ const record = {
275
+ schemaVersion: GOLDEN_RUN_SCHEMA_VERSION,
276
+ runId: runIdFor(directiveVersion, options.model, harness, seeds),
277
+ directiveVersion,
278
+ model: options.model,
279
+ harness,
280
+ seeds,
281
+ corpusVersion: GOLDEN_CORPUS_VERSION,
282
+ recordedAt: now()
283
+ .toISOString()
284
+ .replace(/\.\d{3}Z$/, "Z"),
285
+ results,
286
+ summary,
287
+ };
288
+ if (persist) {
289
+ try {
290
+ persistGoldenRun(projectRoot, record);
291
+ }
292
+ catch (err) {
293
+ return {
294
+ code: 2,
295
+ record,
296
+ message: `eval:run: failed to persist golden run: ${String(err)}`,
297
+ };
298
+ }
299
+ }
300
+ const lines = [
301
+ `eval:run v${record.directiveVersion} model=${record.model} harness=${record.harness} seeds=[${record.seeds.join(",")}]`,
302
+ ` primary pass rate: ${(summary.primaryPassRate * 100).toFixed(1)}% (${summary.primaryTotal} trials)`,
303
+ ` holdout pass rate: ${(summary.holdoutPassRate * 100).toFixed(1)}% (${summary.holdoutTotal} trials)`,
304
+ ` rotating holdout task: ${rotatingHoldout?.id ?? "none"}`,
305
+ ` runId=${record.runId}`,
306
+ ];
307
+ return { code: 0, record, message: lines.join("\n") };
308
+ }
309
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,13 @@
1
+ /** Canonical behavioral event names for the attribution ledger (#1709). */
2
+ export declare const ATTRIBUTION_EVENT_NAMES: {
3
+ readonly valueGateCatch: "value:gate-catch";
4
+ readonly valueWipCapProtect: "value:wip-cap-protect";
5
+ readonly bypassOffFlow: "bypass:off-flow";
6
+ readonly adoptionUnusedCapability: "adoption:unused-capability";
7
+ readonly frictionDirectiveGap: "friction:directive-gap";
8
+ };
9
+ export type AttributionEventName = (typeof ATTRIBUTION_EVENT_NAMES)[keyof typeof ATTRIBUTION_EVENT_NAMES];
10
+ /** Required payload keys per attribution event (merged into lifecycle/events). */
11
+ export declare const ATTRIBUTION_REQUIRED_PAYLOAD: Readonly<Record<string, readonly string[]>>;
12
+ export declare const ALL_ATTRIBUTION_EVENT_NAMES: ("value:gate-catch" | "value:wip-cap-protect" | "bypass:off-flow" | "adoption:unused-capability" | "friction:directive-gap")[];
13
+ //# sourceMappingURL=attribution-constants.d.ts.map
@@ -0,0 +1,18 @@
1
+ /** Canonical behavioral event names for the attribution ledger (#1709). */
2
+ export const ATTRIBUTION_EVENT_NAMES = {
3
+ valueGateCatch: "value:gate-catch",
4
+ valueWipCapProtect: "value:wip-cap-protect",
5
+ bypassOffFlow: "bypass:off-flow",
6
+ adoptionUnusedCapability: "adoption:unused-capability",
7
+ frictionDirectiveGap: "friction:directive-gap",
8
+ };
9
+ /** Required payload keys per attribution event (merged into lifecycle/events). */
10
+ export const ATTRIBUTION_REQUIRED_PAYLOAD = {
11
+ [ATTRIBUTION_EVENT_NAMES.valueGateCatch]: ["signal_class", "source"],
12
+ [ATTRIBUTION_EVENT_NAMES.valueWipCapProtect]: ["signal_class", "source", "count", "cap"],
13
+ [ATTRIBUTION_EVENT_NAMES.bypassOffFlow]: ["signal_class", "source"],
14
+ [ATTRIBUTION_EVENT_NAMES.adoptionUnusedCapability]: ["signal_class", "source", "capability"],
15
+ [ATTRIBUTION_EVENT_NAMES.frictionDirectiveGap]: ["signal_class", "source"],
16
+ };
17
+ export const ALL_ATTRIBUTION_EVENT_NAMES = Object.values(ATTRIBUTION_EVENT_NAMES);
18
+ //# sourceMappingURL=attribution-constants.js.map
@@ -0,0 +1,43 @@
1
+ import { type BehavioralEventRecord } from "../lifecycle/events.js";
2
+ import { type ValueFeedbackResolved } from "../policy/value-feedback.js";
3
+ import { type AttributionEventName } from "./attribution-constants.js";
4
+ export { ALL_ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_REQUIRED_PAYLOAD, type AttributionEventName, } from "./attribution-constants.js";
5
+ /** Four value-attribution signal classes (#1709 RFC). */
6
+ export type SignalClass = "value" | "bypass" | "adoption" | "friction";
7
+ export interface EmitAttributionOptions {
8
+ readonly projectRoot: string;
9
+ readonly logPath?: string | null;
10
+ /** Test hook: skip disk policy read. */
11
+ readonly policyOverride?: ValueFeedbackResolved;
12
+ }
13
+ /**
14
+ * Append an attribution ledger entry when valueFeedback emitEvents is allowed.
15
+ * Returns null when gated OFF (no disk write).
16
+ */
17
+ export declare function emitAttributionSignal(name: AttributionEventName, payload: Record<string, unknown>, options: EmitAttributionOptions): BehavioralEventRecord | null;
18
+ /** Record a detection-bound gate catch (value class). */
19
+ export declare function recordGateCatch(projectRoot: string, source: string, detail: string, options?: {
20
+ logPath?: string | null;
21
+ policyOverride?: ValueFeedbackResolved;
22
+ }): BehavioralEventRecord | null;
23
+ /** Record a WIP-cap protect refusal (value class). */
24
+ export declare function recordWipCapProtect(projectRoot: string, count: number, cap: number, options?: {
25
+ logPath?: string | null;
26
+ policyOverride?: ValueFeedbackResolved;
27
+ }): BehavioralEventRecord | null;
28
+ /** Record a bypass/off-flow signal. */
29
+ export declare function recordBypassSignal(projectRoot: string, source: string, detail: string, options?: {
30
+ logPath?: string | null;
31
+ policyOverride?: ValueFeedbackResolved;
32
+ }): BehavioralEventRecord | null;
33
+ /** Record an adoption/unused-capability signal. */
34
+ export declare function recordAdoptionSignal(projectRoot: string, capability: string, detail: string, options?: {
35
+ logPath?: string | null;
36
+ policyOverride?: ValueFeedbackResolved;
37
+ }): BehavioralEventRecord | null;
38
+ /** Record a friction/directive-gap signal. */
39
+ export declare function recordFrictionSignal(projectRoot: string, source: string, detail: string, options?: {
40
+ logPath?: string | null;
41
+ policyOverride?: ValueFeedbackResolved;
42
+ }): BehavioralEventRecord | null;
43
+ //# sourceMappingURL=attribution-ledger.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { resolve } from "node:path";
2
+ import { DEFAULT_EVENT_LOG, emit } from "../lifecycle/events.js";
3
+ import { isValueFeedbackPathAllowed, resolveValueFeedback, } from "../policy/value-feedback.js";
4
+ import { ATTRIBUTION_EVENT_NAMES } from "./attribution-constants.js";
5
+ export { ALL_ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_REQUIRED_PAYLOAD, } from "./attribution-constants.js";
6
+ function resolveLedgerLogPath(projectRoot, logPath) {
7
+ if (logPath !== undefined && logPath !== null) {
8
+ return resolve(logPath);
9
+ }
10
+ return resolve(projectRoot, DEFAULT_EVENT_LOG);
11
+ }
12
+ function signalClassForEvent(name) {
13
+ const prefix = name.split(":")[0];
14
+ if (prefix === "value" || prefix === "bypass" || prefix === "adoption" || prefix === "friction") {
15
+ return prefix;
16
+ }
17
+ throw new Error(`attribution event '${name}' has unknown signal-class prefix`);
18
+ }
19
+ /**
20
+ * Append an attribution ledger entry when valueFeedback emitEvents is allowed.
21
+ * Returns null when gated OFF (no disk write).
22
+ */
23
+ export function emitAttributionSignal(name, payload, options) {
24
+ try {
25
+ const policy = options.policyOverride ?? resolveValueFeedback(options.projectRoot);
26
+ if (!isValueFeedbackPathAllowed("emitEvents", policy)) {
27
+ return null;
28
+ }
29
+ const signalClass = signalClassForEvent(name);
30
+ const logPath = resolveLedgerLogPath(options.projectRoot, options.logPath);
31
+ return emit(name, {
32
+ signal_class: signalClass,
33
+ ...payload,
34
+ }, { logPath });
35
+ }
36
+ catch {
37
+ // Telemetry is best-effort; any failure must not interrupt gate callers (#1709).
38
+ return null;
39
+ }
40
+ }
41
+ /** Record a detection-bound gate catch (value class). */
42
+ export function recordGateCatch(projectRoot, source, detail, options = {}) {
43
+ return emitAttributionSignal(ATTRIBUTION_EVENT_NAMES.valueGateCatch, { source, detail }, { projectRoot, ...options });
44
+ }
45
+ /** Record a WIP-cap protect refusal (value class). */
46
+ export function recordWipCapProtect(projectRoot, count, cap, options = {}) {
47
+ return emitAttributionSignal(ATTRIBUTION_EVENT_NAMES.valueWipCapProtect, { source: "verify:wip-cap", count, cap }, { projectRoot, ...options });
48
+ }
49
+ /** Record a bypass/off-flow signal. */
50
+ export function recordBypassSignal(projectRoot, source, detail, options = {}) {
51
+ return emitAttributionSignal(ATTRIBUTION_EVENT_NAMES.bypassOffFlow, { source, detail }, { projectRoot, ...options });
52
+ }
53
+ /** Record an adoption/unused-capability signal. */
54
+ export function recordAdoptionSignal(projectRoot, capability, detail, options = {}) {
55
+ return emitAttributionSignal(ATTRIBUTION_EVENT_NAMES.adoptionUnusedCapability, { source: "adoption-registry", capability, detail }, { projectRoot, ...options });
56
+ }
57
+ /** Record a friction/directive-gap signal. */
58
+ export function recordFrictionSignal(projectRoot, source, detail, options = {}) {
59
+ return emitAttributionSignal(ATTRIBUTION_EVENT_NAMES.frictionDirectiveGap, { source, detail }, { projectRoot, ...options });
60
+ }
61
+ //# sourceMappingURL=attribution-ledger.js.map
package/dist/index.d.ts CHANGED
@@ -15,6 +15,11 @@ export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
16
  export * as doctor from "./doctor/index.js";
17
17
  export * from "./encoding/index.js";
18
+ export * as evalCrud from "./eval/crud-telemetry.js";
19
+ export * as evalHealth from "./eval/health.js";
20
+ export * as evalReport from "./eval/report.js";
21
+ export * as evalRun from "./eval/run.js";
22
+ export * as events from "./events/attribution-ledger.js";
18
23
  export * from "./forward-coverage/evaluate.js";
19
24
  export * as intake from "./intake/index.js";
20
25
  export * as layout from "./layout/index.js";
package/dist/index.js CHANGED
@@ -15,6 +15,11 @@ export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
16
  export * as doctor from "./doctor/index.js";
17
17
  export * from "./encoding/index.js";
18
+ export * as evalCrud from "./eval/crud-telemetry.js";
19
+ export * as evalHealth from "./eval/health.js";
20
+ export * as evalReport from "./eval/report.js";
21
+ export * as evalRun from "./eval/run.js";
22
+ export * as events from "./events/attribution-ledger.js";
18
23
  export * from "./forward-coverage/evaluate.js";
19
24
  export * as intake from "./intake/index.js";
20
25
  export * as layout from "./layout/index.js";
@@ -26,11 +26,11 @@ export const CANONICAL_GITIGNORE_BASELINE = [
26
26
  ".deft/ritual-state.json",
27
27
  ".deft/last-session.json",
28
28
  ".deft/routing.local.json",
29
- "vbrief/.eval/candidates.jsonl",
30
- "vbrief/.eval/summary-history.jsonl",
31
- "vbrief/.eval/scope-lifecycle.jsonl",
32
- "vbrief/.eval/decompositions/",
33
- "vbrief/.eval/doctor-state.json",
29
+ "vbrief/.triage-cache/candidates.jsonl",
30
+ "vbrief/.triage-cache/summary-history.jsonl",
31
+ "vbrief/.triage-cache/scope-lifecycle.jsonl",
32
+ "vbrief/.triage-cache/decompositions/",
33
+ "vbrief/.triage-cache/doctor-state.json",
34
34
  "vbrief/*.lock",
35
35
  ".deft/core.bak-*/",
36
36
  ".deft/*.bak-*",
@@ -8,6 +8,7 @@
8
8
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
9
9
  import { homedir, platform } from "node:os";
10
10
  import { join, resolve } from "node:path";
11
+ import { assertDepositContained } from "../deposit/contain.js";
11
12
  import { copyTree } from "../deposit/copy-tree.js";
12
13
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
13
14
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
@@ -124,6 +125,11 @@ export async function runInitDeposit(args, io, seams = {}) {
124
125
  if (legacy.legacy) {
125
126
  throw new LegacyLayoutRefusedError(legacy);
126
127
  }
128
+ // #2305: refuse a symlink-escaping deposit boundary BEFORE the first
129
+ // copy/reconstitute/mkdir, so a malicious `.deft`/`.deft/core` symlink cannot
130
+ // redirect the deposit outside the resolved project tree. Deposits nothing on
131
+ // refusal.
132
+ assertDepositContained(projectDir, deftDir);
127
133
  const resolveContent = seams.resolveContentRoot ?? resolveInstalledContentRoot;
128
134
  const copyContent = seams.copyContent ?? copyTree;
129
135
  const contentRoot = await resolveContent();
@@ -10,6 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
11
  import { platform as osPlatform } from "node:os";
12
12
  import { join, resolve } from "node:path";
13
+ import { assertDepositContained } from "../deposit/contain.js";
13
14
  import { copyTree } from "../deposit/copy-tree.js";
14
15
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
15
16
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
@@ -335,6 +336,10 @@ export async function runRefreshDeposit(args, io, seams = {}) {
335
336
  if (legacy.legacy) {
336
337
  throw new LegacyLayoutRefusedError(legacy);
337
338
  }
339
+ // #2305: refuse a symlink-escaping deposit boundary BEFORE the first copy so a
340
+ // malicious `.deft`/`.deft/core` symlink cannot redirect the refresh outside
341
+ // the resolved project tree. Writes nothing on refusal.
342
+ assertDepositContained(projectDir, deftDir);
338
343
  const resolveContent = seams.resolveContentRoot ?? resolveInstalledContentRoot;
339
344
  const copyContent = seams.copyContent ?? copyTree;
340
345
  const readEngine = seams.readEngineVersion ?? readCorePackageVersion;
@@ -1,7 +1,18 @@
1
+ import { type ScanFlag } from "../cache/scanner.js";
1
2
  import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
2
3
  import { type ScmCallFn } from "./reconcile-issues.js";
3
4
  export declare const INGEST_STATUSES: readonly ["proposed", "pending", "active"];
4
5
  export type IngestStatus = (typeof INGEST_STATUSES)[number];
6
+ /**
7
+ * Thrown when the quarantine scanner hard-fails (credential-shaped content) on
8
+ * an ingested issue body/comment thread (#2306). Ingest MUST fail closed: emit
9
+ * nothing and propagate a non-zero exit rather than persisting the xBRIEF.
10
+ */
11
+ export declare class ScannerHardFailError extends Error {
12
+ readonly issueNumber: number;
13
+ readonly flags: readonly ScanFlag[];
14
+ constructor(issueNumber: number, flags: readonly ScanFlag[]);
15
+ }
5
16
  /** GitHub issue comment thread entry (REST `repos/.../issues/N/comments`). */
6
17
  export interface IssueComment {
7
18
  readonly id?: number;