@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,307 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { readCorePackageVersion } from "../engine-version.js";
4
+ import { resolveEvalPath } from "../layout/resolve.js";
5
+ import { scanVbrief } from "../vbrief-validate/conformance.js";
6
+ import { validateVbriefSchema } from "../vbrief-validate/schema.js";
7
+ export const CRUD_METRICS_HISTORY_REL = "results/crud-metrics.jsonl";
8
+ /** Whole-file rewrites change at least half of the bytes or re-serialize with high drift. */
9
+ export const BYTE_DIFF_WHOLE_FILE_THRESHOLD = 0.5;
10
+ function isRecord(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ /** Edit distance between two UTF-8 strings (Levenshtein, two-row). */
14
+ function levenshteinDistance(before, after) {
15
+ if (before === after) {
16
+ return 0;
17
+ }
18
+ if (before.length === 0) {
19
+ return after.length;
20
+ }
21
+ if (after.length === 0) {
22
+ return before.length;
23
+ }
24
+ let prev = Array.from({ length: after.length + 1 }, (_, index) => index);
25
+ let curr = new Array(after.length + 1).fill(0);
26
+ for (let row = 1; row <= before.length; row += 1) {
27
+ curr[0] = row;
28
+ for (let col = 1; col <= after.length; col += 1) {
29
+ const cost = before[row - 1] === after[col - 1] ? 0 : 1;
30
+ const up = prev[col] ?? 0;
31
+ const left = curr[col - 1] ?? 0;
32
+ const diag = prev[col - 1] ?? 0;
33
+ curr[col] = Math.min(up + 1, left + 1, diag + cost);
34
+ }
35
+ [prev, curr] = [curr, prev];
36
+ }
37
+ return prev[after.length] ?? 0;
38
+ }
39
+ /** Ratio of edit distance to the longer string length. */
40
+ export function computeChangedByteRatio(before, after) {
41
+ const maxLen = Math.max(before.length, after.length);
42
+ if (maxLen === 0) {
43
+ return 0;
44
+ }
45
+ return levenshteinDistance(before, after) / maxLen;
46
+ }
47
+ /** Classify whether an update preserved bytes (surgical) or rewrote the file. */
48
+ export function classifyByteDiffMinimality(before, after) {
49
+ const changedRatio = computeChangedByteRatio(before, after);
50
+ try {
51
+ const oldParsed = JSON.parse(before);
52
+ const newParsed = JSON.parse(after);
53
+ if (JSON.stringify(oldParsed) === JSON.stringify(newParsed) && changedRatio > 0) {
54
+ return { kind: "whole-file-rewrite", changedRatio };
55
+ }
56
+ }
57
+ catch {
58
+ // Fall through to byte-ratio classification.
59
+ }
60
+ if (changedRatio >= BYTE_DIFF_WHOLE_FILE_THRESHOLD) {
61
+ return { kind: "whole-file-rewrite", changedRatio };
62
+ }
63
+ return { kind: "surgical", changedRatio };
64
+ }
65
+ function assessDocument(path, parsed) {
66
+ if (!isRecord(parsed)) {
67
+ return {
68
+ schemaValid: false,
69
+ schemaErrors: [`${path}: document must be a JSON object`],
70
+ inventedKeys: [],
71
+ };
72
+ }
73
+ const schemaErrors = validateVbriefSchema(parsed, path);
74
+ const findings = scanVbrief(path, parsed);
75
+ const inventedKeys = findings.map((finding) => finding.key);
76
+ return {
77
+ schemaValid: schemaErrors.length === 0,
78
+ schemaErrors,
79
+ inventedKeys,
80
+ };
81
+ }
82
+ function sanitizeInlineMessage(message) {
83
+ return message.replace(/\r?\n/g, " ");
84
+ }
85
+ function ensureParentDir(path) {
86
+ mkdirSync(dirname(path), { recursive: true });
87
+ }
88
+ /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1). */
89
+ export function crudMetricsHistoryPath(projectRoot) {
90
+ return resolveEvalPath(projectRoot, CRUD_METRICS_HISTORY_REL);
91
+ }
92
+ /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1). */
93
+ export function persistCrudMetrics(projectRoot, metrics) {
94
+ if (metrics.length === 0) {
95
+ return;
96
+ }
97
+ const path = crudMetricsHistoryPath(projectRoot);
98
+ mkdirSync(dirname(path), { recursive: true });
99
+ for (const metric of metrics) {
100
+ appendFileSync(path, `${JSON.stringify(metric)}\n`, "utf8");
101
+ }
102
+ }
103
+ /**
104
+ * Instrumented vBRIEF/xBRIEF CRUD chokepoint (#1703 Tier 1).
105
+ * Every operation emits a version-tagged metric covering schema validity,
106
+ * field invention, and (for updates) byte-diff minimality.
107
+ * Delete intentionally succeeds on schema-invalid files so corrupt artifacts can be removed.
108
+ */
109
+ export class InstrumentedVbriefCrud {
110
+ directiveVersion;
111
+ now;
112
+ metrics = [];
113
+ constructor(options = {}) {
114
+ this.directiveVersion = options.directiveVersion ?? readCorePackageVersion();
115
+ this.now = options.now ?? (() => new Date());
116
+ }
117
+ getMetrics() {
118
+ return this.metrics;
119
+ }
120
+ clearMetrics() {
121
+ this.metrics.length = 0;
122
+ }
123
+ create(path, content) {
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(content);
127
+ }
128
+ catch (err) {
129
+ const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
130
+ this.recordMetric({
131
+ operation: "create",
132
+ path,
133
+ schemaValid: false,
134
+ schemaErrors: [`${path}: invalid JSON -- ${message}`],
135
+ inventedKeys: [],
136
+ byteDiffMinimality: null,
137
+ byteDiffChangedRatio: null,
138
+ });
139
+ return { ok: false, error: message };
140
+ }
141
+ const assessment = assessDocument(path, parsed);
142
+ this.recordMetric({
143
+ operation: "create",
144
+ path,
145
+ schemaValid: assessment.schemaValid,
146
+ schemaErrors: assessment.schemaErrors,
147
+ inventedKeys: assessment.inventedKeys,
148
+ byteDiffMinimality: null,
149
+ byteDiffChangedRatio: null,
150
+ });
151
+ if (!assessment.schemaValid) {
152
+ return { ok: false, error: assessment.schemaErrors.join("; ") };
153
+ }
154
+ ensureParentDir(path);
155
+ writeFileSync(path, content, "utf8");
156
+ return { ok: true };
157
+ }
158
+ read(path) {
159
+ if (!existsSync(path)) {
160
+ this.recordMetric({
161
+ operation: "read",
162
+ path,
163
+ schemaValid: false,
164
+ schemaErrors: [`${path}: file not found`],
165
+ inventedKeys: [],
166
+ byteDiffMinimality: null,
167
+ byteDiffChangedRatio: null,
168
+ });
169
+ return { ok: false, error: "ENOENT" };
170
+ }
171
+ const content = readFileSync(path, "utf8");
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(content);
175
+ }
176
+ catch (err) {
177
+ const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
178
+ this.recordMetric({
179
+ operation: "read",
180
+ path,
181
+ schemaValid: false,
182
+ schemaErrors: [`${path}: invalid JSON -- ${message}`],
183
+ inventedKeys: [],
184
+ byteDiffMinimality: null,
185
+ byteDiffChangedRatio: null,
186
+ });
187
+ return { ok: false, error: message };
188
+ }
189
+ const assessment = assessDocument(path, parsed);
190
+ this.recordMetric({
191
+ operation: "read",
192
+ path,
193
+ schemaValid: assessment.schemaValid,
194
+ schemaErrors: assessment.schemaErrors,
195
+ inventedKeys: assessment.inventedKeys,
196
+ byteDiffMinimality: null,
197
+ byteDiffChangedRatio: null,
198
+ });
199
+ if (!assessment.schemaValid) {
200
+ return { ok: false, error: assessment.schemaErrors.join("; ") };
201
+ }
202
+ return { ok: true, content };
203
+ }
204
+ update(path, content, options = {}) {
205
+ const previousBytes = options.trustedWrite || !existsSync(path) ? null : readFileSync(path, "utf8");
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(content);
209
+ }
210
+ catch (err) {
211
+ const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
212
+ this.recordMetric({
213
+ operation: "update",
214
+ path,
215
+ schemaValid: false,
216
+ schemaErrors: [`${path}: invalid JSON -- ${message}`],
217
+ inventedKeys: [],
218
+ byteDiffMinimality: null,
219
+ byteDiffChangedRatio: null,
220
+ });
221
+ return { ok: false, error: message };
222
+ }
223
+ const assessment = assessDocument(path, parsed);
224
+ let byteDiffMinimality = null;
225
+ let byteDiffChangedRatio = null;
226
+ if (previousBytes !== null) {
227
+ const diff = classifyByteDiffMinimality(previousBytes, content);
228
+ byteDiffMinimality = diff.kind;
229
+ byteDiffChangedRatio = diff.changedRatio;
230
+ }
231
+ this.recordMetric({
232
+ operation: "update",
233
+ path,
234
+ schemaValid: assessment.schemaValid,
235
+ schemaErrors: assessment.schemaErrors,
236
+ inventedKeys: assessment.inventedKeys,
237
+ byteDiffMinimality,
238
+ byteDiffChangedRatio,
239
+ });
240
+ if (!assessment.schemaValid && !options.trustedWrite) {
241
+ return { ok: false, error: assessment.schemaErrors.join("; ") };
242
+ }
243
+ ensureParentDir(path);
244
+ writeFileSync(path, content, "utf8");
245
+ return { ok: true };
246
+ }
247
+ delete(path) {
248
+ if (!existsSync(path)) {
249
+ this.recordMetric({
250
+ operation: "delete",
251
+ path,
252
+ schemaValid: false,
253
+ schemaErrors: [`${path}: file not found`],
254
+ inventedKeys: [],
255
+ byteDiffMinimality: null,
256
+ byteDiffChangedRatio: null,
257
+ });
258
+ return { ok: false, error: "ENOENT" };
259
+ }
260
+ const content = readFileSync(path, "utf8");
261
+ let parsed;
262
+ try {
263
+ parsed = JSON.parse(content);
264
+ }
265
+ catch (err) {
266
+ const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
267
+ this.recordMetric({
268
+ operation: "delete",
269
+ path,
270
+ schemaValid: false,
271
+ schemaErrors: [`${path}: invalid JSON -- ${message}`],
272
+ inventedKeys: [],
273
+ byteDiffMinimality: null,
274
+ byteDiffChangedRatio: null,
275
+ });
276
+ unlinkSync(path);
277
+ return { ok: true };
278
+ }
279
+ const assessment = assessDocument(path, parsed);
280
+ this.recordMetric({
281
+ operation: "delete",
282
+ path,
283
+ schemaValid: assessment.schemaValid,
284
+ schemaErrors: assessment.schemaErrors,
285
+ inventedKeys: assessment.inventedKeys,
286
+ byteDiffMinimality: null,
287
+ byteDiffChangedRatio: null,
288
+ });
289
+ unlinkSync(path);
290
+ return { ok: true };
291
+ }
292
+ recordMetric(input) {
293
+ this.metrics.push({
294
+ directiveVersion: this.directiveVersion,
295
+ operation: input.operation,
296
+ path: input.path,
297
+ schemaValid: input.schemaValid,
298
+ schemaErrors: input.schemaErrors,
299
+ fieldInventionCount: input.inventedKeys.length,
300
+ inventedKeys: input.inventedKeys,
301
+ byteDiffMinimality: input.byteDiffMinimality,
302
+ byteDiffChangedRatio: input.byteDiffChangedRatio,
303
+ recordedAt: this.now().toISOString(),
304
+ });
305
+ }
306
+ }
307
+ //# sourceMappingURL=crud-telemetry.js.map
@@ -0,0 +1,52 @@
1
+ export declare const HEALTH_SCHEMA_VERSION: 1;
2
+ export declare const HEALTH_HISTORY_REL = "results/health-history.jsonl";
3
+ /** One static Tier-0 gate probe aggregated into the health score. */
4
+ export interface GateProbeResult {
5
+ readonly id: string;
6
+ readonly title: string;
7
+ readonly pass: boolean;
8
+ readonly exitCode: number;
9
+ readonly detail?: string;
10
+ readonly skipped?: boolean;
11
+ readonly skipReason?: string;
12
+ }
13
+ /** Evidence for a contradictory / unsatisfiable gate pair (#1694 class). */
14
+ export interface ContradictionEvidence {
15
+ readonly id: string;
16
+ readonly kind: "unsatisfiable-nudge";
17
+ readonly summary: string;
18
+ readonly signals: readonly string[];
19
+ }
20
+ /** Versioned Tier-0 health run persisted for trending (#1703). */
21
+ export interface HealthReport {
22
+ readonly schemaVersion: typeof HEALTH_SCHEMA_VERSION;
23
+ readonly version: string;
24
+ readonly recordedAt: string;
25
+ readonly score: number;
26
+ readonly gates: readonly GateProbeResult[];
27
+ readonly contradictions: readonly ContradictionEvidence[];
28
+ }
29
+ export interface EvaluateHealthOptions {
30
+ readonly projectRoot?: string;
31
+ readonly persist?: boolean;
32
+ readonly now?: () => Date;
33
+ readonly frameworkSource?: boolean;
34
+ }
35
+ export interface EvaluateHealthResult {
36
+ readonly code: 0 | 1 | 2;
37
+ readonly report: HealthReport | null;
38
+ readonly message: string;
39
+ }
40
+ /** Absolute path to the versioned health history ledger. */
41
+ export declare function healthHistoryPath(projectRoot: string): string;
42
+ /** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
43
+ export declare function detectWipCapUnsatisfiableNudge(projectRoot: string): ContradictionEvidence | null;
44
+ /** Run all registered contradictory-gate detectors. */
45
+ export declare function detectContradictoryGates(projectRoot: string): ContradictionEvidence[];
46
+ /** Compute a 0-100 score from gate pass rate minus contradiction penalty. */
47
+ export declare function computeHealthScore(gates: readonly GateProbeResult[], contradictions: readonly ContradictionEvidence[]): number;
48
+ /** Append one health run to the versioned ledger (#1703 Tier 0). */
49
+ export declare function persistHealthRun(projectRoot: string, report: HealthReport): void;
50
+ /** Aggregate Tier-0 static gates into a versioned framework health score (#1703). */
51
+ export declare function evaluateHealth(options?: EvaluateHealthOptions): EvaluateHealthResult;
52
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1,240 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { agentsRefreshPlan } from "../doctor/agents-md.js";
4
+ import { evaluate as evaluateEncoding } from "../encoding/evaluate.js";
5
+ import { readCorePackageVersion } from "../engine-version.js";
6
+ import { resolveEvalPath, resolveProjectDefinitionPath } from "../layout/resolve.js";
7
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
8
+ import { classifyOnboarding, detectPriorState } from "../triage/welcome/prior-state.js";
9
+ import { validateLinks } from "../validate-content/index.js";
10
+ import { evaluateConformance } from "../vbrief-validate/conformance.js";
11
+ import { validateWipCapOnPlan } from "../vbrief-validate/plan-hooks.js";
12
+ import { evaluateContentManifest } from "../verify-source/content-manifest.js";
13
+ export const HEALTH_SCHEMA_VERSION = 1;
14
+ export const HEALTH_HISTORY_REL = "results/health-history.jsonl";
15
+ function sanitizeOneLine(value) {
16
+ return value.replace(/\r?\n/g, " ");
17
+ }
18
+ function isFrameworkSourceCheckout(projectRoot) {
19
+ const manifest = resolve(projectRoot, "conventions/content-manifest.json");
20
+ const cliPkg = resolve(projectRoot, "packages/cli/package.json");
21
+ return existsSync(manifest) && existsSync(cliPkg);
22
+ }
23
+ function loadPlan(projectRoot) {
24
+ const filepath = resolveProjectDefinitionPath(projectRoot);
25
+ if (!existsSync(filepath)) {
26
+ return null;
27
+ }
28
+ try {
29
+ const data = JSON.parse(readFileSync(filepath, "utf8"));
30
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
31
+ return null;
32
+ }
33
+ return { plan: data.plan, filepath };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function probeEncoding(projectRoot) {
40
+ const result = evaluateEncoding(projectRoot, { mode: "all" });
41
+ return {
42
+ id: "encoding",
43
+ title: "verify:encoding",
44
+ pass: result.exitCode === 0,
45
+ exitCode: result.exitCode,
46
+ detail: result.exitCode === 0 ? undefined : result.message.split("\n")[0],
47
+ };
48
+ }
49
+ function probeLinks(projectRoot) {
50
+ const result = validateLinks.evaluate({
51
+ cwd: projectRoot,
52
+ strict: false,
53
+ argv: [],
54
+ linkCheckStrict: process.env.LINK_CHECK_STRICT === "1",
55
+ });
56
+ return {
57
+ id: "links",
58
+ title: "verify:links",
59
+ pass: result.code === 0,
60
+ exitCode: result.code,
61
+ detail: result.code === 0 ? undefined : result.message.split("\n")[0],
62
+ };
63
+ }
64
+ function probeVbriefConformance(projectRoot) {
65
+ const result = evaluateConformance(projectRoot, { mode: "all" });
66
+ return {
67
+ id: "vbrief-conformance",
68
+ title: "verify:vbrief-conformance",
69
+ pass: result.exitCode === 0,
70
+ exitCode: result.exitCode,
71
+ detail: result.exitCode === 0 ? undefined : result.message.split("\n")[0],
72
+ };
73
+ }
74
+ function probeAgentsMdFreshness(projectRoot) {
75
+ const plan = agentsRefreshPlan(projectRoot);
76
+ const state = String(plan.state ?? "unknown");
77
+ const pass = state === "current";
78
+ return {
79
+ id: "agents-md-freshness",
80
+ title: "AGENTS.md managed-section freshness",
81
+ pass,
82
+ exitCode: pass ? 0 : 1,
83
+ detail: pass ? undefined : `state=${state}`,
84
+ };
85
+ }
86
+ function probeContentManifest(projectRoot) {
87
+ const result = evaluateContentManifest(projectRoot, { root: projectRoot });
88
+ return {
89
+ id: "content-manifest",
90
+ title: "verify:content-manifest",
91
+ pass: result.code === 0,
92
+ exitCode: result.code,
93
+ detail: result.code === 0 ? undefined : result.message.split("\n")[0],
94
+ };
95
+ }
96
+ /** Absolute path to the versioned health history ledger. */
97
+ export function healthHistoryPath(projectRoot) {
98
+ return resolveEvalPath(projectRoot, HEALTH_HISTORY_REL);
99
+ }
100
+ /** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
101
+ export function detectWipCapUnsatisfiableNudge(projectRoot) {
102
+ const loaded = loadPlan(projectRoot);
103
+ if (loaded === null) {
104
+ return null;
105
+ }
106
+ const { plan, filepath } = loaded;
107
+ const policy = readPlanPolicy(plan);
108
+ const wipCapPresent = typeof policy === "object" &&
109
+ policy !== null &&
110
+ !Array.isArray(policy) &&
111
+ "wipCap" in policy;
112
+ if (wipCapPresent) {
113
+ return null;
114
+ }
115
+ const state = detectPriorState(projectRoot);
116
+ const [, missing] = classifyOnboarding(state);
117
+ if (!missing.includes("wipCap")) {
118
+ return null;
119
+ }
120
+ const validatorErrors = validateWipCapOnPlan(plan, filepath);
121
+ if (validatorErrors.length > 0) {
122
+ return null;
123
+ }
124
+ return {
125
+ id: "wipCap-unsatisfiable-nudge",
126
+ kind: "unsatisfiable-nudge",
127
+ summary: "Onboarding completeness treats absent plan.policy.wipCap as incomplete, but omit-by-design accepts absence as valid (#1694 / #1186 D1).",
128
+ signals: [
129
+ "classifyOnboarding: wipCap listed in missing onboarding signals",
130
+ "validateWipCapOnPlan: omitted wipCap is valid",
131
+ "triage:welcome --onboard nudge cannot clear without violating omit-by-design contract",
132
+ ],
133
+ };
134
+ }
135
+ /** Run all registered contradictory-gate detectors. */
136
+ export function detectContradictoryGates(projectRoot) {
137
+ const wipCap = detectWipCapUnsatisfiableNudge(projectRoot);
138
+ return wipCap === null ? [] : [wipCap];
139
+ }
140
+ /** Compute a 0-100 score from gate pass rate minus contradiction penalty. */
141
+ export function computeHealthScore(gates, contradictions) {
142
+ const active = gates.filter((g) => !g.skipped);
143
+ if (active.length === 0) {
144
+ return contradictions.length > 0 ? 0 : 100;
145
+ }
146
+ const passed = active.filter((g) => g.pass).length;
147
+ const base = Math.round((passed / active.length) * 100);
148
+ const penalty = contradictions.length * 15;
149
+ return Math.max(0, base - penalty);
150
+ }
151
+ /** Append one health run to the versioned ledger (#1703 Tier 0). */
152
+ export function persistHealthRun(projectRoot, report) {
153
+ const path = healthHistoryPath(projectRoot);
154
+ mkdirSync(dirname(path), { recursive: true });
155
+ appendFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
156
+ }
157
+ function collectStaticGates(projectRoot, frameworkSource) {
158
+ const gates = [
159
+ probeEncoding(projectRoot),
160
+ probeLinks(projectRoot),
161
+ probeVbriefConformance(projectRoot),
162
+ probeAgentsMdFreshness(projectRoot),
163
+ ];
164
+ if (frameworkSource) {
165
+ gates.push(probeContentManifest(projectRoot));
166
+ }
167
+ else {
168
+ gates.push({
169
+ id: "content-manifest",
170
+ title: "verify:content-manifest",
171
+ pass: true,
172
+ exitCode: 0,
173
+ skipped: true,
174
+ skipReason: "framework-source-only gate",
175
+ });
176
+ }
177
+ return gates;
178
+ }
179
+ function formatHumanReport(report) {
180
+ const lines = [
181
+ `eval:health v${report.version} score=${report.score}/100 (${report.recordedAt})`,
182
+ ...report.gates.map((g) => {
183
+ const status = g.skipped ? "SKIP" : g.pass ? "PASS" : "FAIL";
184
+ const suffix = g.skipped
185
+ ? ` (${sanitizeOneLine(g.skipReason ?? "")})`
186
+ : g.detail
187
+ ? ` -- ${sanitizeOneLine(g.detail)}`
188
+ : "";
189
+ return ` [${status}] ${g.title}${suffix}`;
190
+ }),
191
+ ];
192
+ if (report.contradictions.length > 0) {
193
+ lines.push(" Contradictory gates:");
194
+ for (const c of report.contradictions) {
195
+ lines.push(` - ${c.id}: ${sanitizeOneLine(c.summary)}`);
196
+ }
197
+ }
198
+ return lines.join("\n");
199
+ }
200
+ /** Aggregate Tier-0 static gates into a versioned framework health score (#1703). */
201
+ export function evaluateHealth(options = {}) {
202
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
203
+ const frameworkSource = options.frameworkSource ?? isFrameworkSourceCheckout(projectRoot);
204
+ const now = options.now ?? (() => new Date());
205
+ const persist = options.persist ?? true;
206
+ const gates = collectStaticGates(projectRoot, frameworkSource);
207
+ const contradictions = detectContradictoryGates(projectRoot);
208
+ const score = computeHealthScore(gates, contradictions);
209
+ const report = {
210
+ schemaVersion: HEALTH_SCHEMA_VERSION,
211
+ version: readCorePackageVersion(),
212
+ recordedAt: now()
213
+ .toISOString()
214
+ .replace(/\.\d{3}Z$/, "Z"),
215
+ score,
216
+ gates,
217
+ contradictions,
218
+ };
219
+ if (persist) {
220
+ try {
221
+ persistHealthRun(projectRoot, report);
222
+ }
223
+ catch (err) {
224
+ const persistError = `eval:health: failed to persist health history: ${String(err)}`;
225
+ const healthy = score === 100 && contradictions.length === 0;
226
+ return {
227
+ code: healthy ? 0 : 1,
228
+ report,
229
+ message: `${formatHumanReport(report)}\n${persistError}`,
230
+ };
231
+ }
232
+ }
233
+ const healthy = score === 100 && contradictions.length === 0;
234
+ return {
235
+ code: healthy ? 0 : 1,
236
+ report,
237
+ message: formatHumanReport(report),
238
+ };
239
+ }
240
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,38 @@
1
+ import { type HealthReport } from "./health.js";
2
+ /** Repeat-suppression window for the budgeted eval session nudge (#1703 / #1279 parity). */
3
+ export declare const EVAL_READBACK_SUPPRESSION_HOURS = 4;
4
+ export declare const EVAL_READBACK_HISTORY_REL: string;
5
+ export declare const EVAL_READBACK_HISTORY_SCHEMA = "deft.eval.readback.v1";
6
+ export interface SessionEvalReadbackResult {
7
+ readonly line: string | null;
8
+ readonly suppressed: boolean;
9
+ }
10
+ /** Stable nudge key for debounce — score + contradiction ids. */
11
+ export declare function evalReadbackNudgeKey(report: HealthReport): string;
12
+ /** True when the same eval nudge was read back within the suppression window. */
13
+ export declare function shouldSuppressEvalReadback(nudgeKey: string, historyFile: string, options?: {
14
+ now?: Date;
15
+ }): boolean;
16
+ /** Whether the current health report warrants a session-start advisory nudge (#2336). */
17
+ export declare function shouldNudgeEvalHealth(report: HealthReport, previous: HealthReport | null): boolean;
18
+ export declare function formatEvalHealthSessionLine(report: HealthReport, previous: HealthReport | null): string;
19
+ export interface RenderSessionEvalReadbackOptions {
20
+ readonly now?: Date;
21
+ readonly maxChars?: number;
22
+ readonly writeHistory?: boolean;
23
+ readonly evaluate?: (projectRoot: string) => {
24
+ report: HealthReport | null;
25
+ };
26
+ }
27
+ /** Budgeted session eval one-liner — silent when healthy or debounced (#1703 / #2336). */
28
+ export declare function renderSessionEvalReadback(projectRoot: string, options?: RenderSessionEvalReadbackOptions): SessionEvalReadbackResult;
29
+ /** Emit the eval session readback line when present. Returns the line or null. */
30
+ export declare function emitSessionEvalReadback(projectRoot: string, options?: {
31
+ output?: (line: string) => void;
32
+ now?: Date;
33
+ writeHistory?: boolean;
34
+ evaluate?: (projectRoot: string) => {
35
+ report: HealthReport | null;
36
+ };
37
+ }): string | null;
38
+ //# sourceMappingURL=readback.d.ts.map