@deftai/directive-core 0.75.0 → 0.77.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 (74) hide show
  1. package/dist/agents-md-budget/evaluate.d.ts +8 -1
  2. package/dist/agents-md-budget/evaluate.js +77 -22
  3. package/dist/agents-md-budget/skill-frontmatter.d.ts +2 -0
  4. package/dist/agents-md-budget/skill-frontmatter.js +4 -3
  5. package/dist/cache/operations.d.ts +2 -0
  6. package/dist/cache/operations.js +16 -1
  7. package/dist/content-contracts/skills/skill-frontmatter.js +4 -0
  8. package/dist/doctor/constants.js +3 -3
  9. package/dist/doctor/index.d.ts +1 -0
  10. package/dist/doctor/index.js +1 -0
  11. package/dist/doctor/main.js +2 -1
  12. package/dist/doctor/payload-staleness.js +62 -63
  13. package/dist/doctor/release-availability.d.ts +28 -0
  14. package/dist/doctor/release-availability.js +35 -0
  15. package/dist/eval/crud-telemetry.d.ts +5 -4
  16. package/dist/eval/crud-telemetry.js +9 -5
  17. package/dist/eval/health.d.ts +5 -4
  18. package/dist/eval/health.js +24 -16
  19. package/dist/eval/readback.js +2 -8
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/init-deposit/scaffold.js +28 -17
  23. package/dist/intake/issue-ingest.js +23 -4
  24. package/dist/metrics/index.d.ts +2 -0
  25. package/dist/metrics/index.js +2 -0
  26. package/dist/metrics/resolve-metrics-home.d.ts +50 -0
  27. package/dist/metrics/resolve-metrics-home.js +125 -0
  28. package/dist/packaging/openpackage-tiers.d.ts +12 -0
  29. package/dist/packaging/openpackage-tiers.js +42 -0
  30. package/dist/plan-sequence/index.d.ts +3 -0
  31. package/dist/plan-sequence/index.js +3 -0
  32. package/dist/plan-sequence/store.d.ts +6 -0
  33. package/dist/plan-sequence/store.js +29 -0
  34. package/dist/plan-sequence/types.d.ts +79 -0
  35. package/dist/plan-sequence/types.js +220 -0
  36. package/dist/preflight/evaluate.d.ts +5 -2
  37. package/dist/preflight/evaluate.js +9 -4
  38. package/dist/preflight/index.d.ts +1 -1
  39. package/dist/preflight/index.js +1 -1
  40. package/dist/release/pipeline-fixture.d.ts +3 -0
  41. package/dist/release/pipeline-fixture.js +11 -0
  42. package/dist/release/pipeline.js +4 -0
  43. package/dist/release/version.d.ts +2 -0
  44. package/dist/release/version.js +42 -0
  45. package/dist/release-e2e/npm-ops.js +20 -6
  46. package/dist/scope/transition.js +17 -2
  47. package/dist/scope/vbrief-ref.d.ts +6 -0
  48. package/dist/scope/vbrief-ref.js +21 -2
  49. package/dist/session/index.d.ts +1 -0
  50. package/dist/session/index.js +1 -0
  51. package/dist/session/posture.d.ts +50 -0
  52. package/dist/session/posture.js +152 -0
  53. package/dist/session/ritual-sentinel.d.ts +2 -0
  54. package/dist/session/ritual-sentinel.js +3 -0
  55. package/dist/session/session-start.d.ts +8 -0
  56. package/dist/session/session-start.js +43 -0
  57. package/dist/session/verify-session-ritual.d.ts +6 -0
  58. package/dist/session/verify-session-ritual.js +67 -4
  59. package/dist/triage/bootstrap/gitignore.js +8 -4
  60. package/dist/triage/cache-path.js +16 -3
  61. package/dist/triage/welcome/writers.js +2 -0
  62. package/dist/ts-check-lane/index.d.ts +1 -1
  63. package/dist/ts-check-lane/index.js +1 -1
  64. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  65. package/dist/ts-check-lane/run-lane.js +15 -0
  66. package/dist/vbrief-build/constants.d.ts +3 -3
  67. package/dist/vbrief-build/constants.js +4 -3
  68. package/dist/vbrief-build/index.d.ts +1 -1
  69. package/dist/vbrief-build/index.js +1 -1
  70. package/dist/verify-env/command-spawn.d.ts +24 -0
  71. package/dist/verify-env/command-spawn.js +78 -0
  72. package/dist/verify-env/index.d.ts +1 -0
  73. package/dist/verify-env/index.js +1 -0
  74. package/package.json +7 -3
@@ -4,7 +4,11 @@ export type OutputStream = "stdout" | "stderr" | "none";
4
4
  /**
5
5
  * Layered absolute north-star for the always-on managed surface (#2372 / #2450 / #2452).
6
6
  *
7
- * `ABSOLUTE_MANAGED_MAX_BYTES` is the relocation north-star (<=8192 B / ~2k tok).
7
+ * `ABSOLUTE_MANAGED_MAX_BYTES` is the managed-section relocation north-star (<=8192 B / ~2k tok)
8
+ * — Phase-2 success bar. `COMBINED_ALWAYS_ON_MAX_BYTES` is the Phase-3 combined always-on
9
+ * north-star (managed + DD-3 + hooks), raised to 9 KB after diminishing returns on further
10
+ * thinning (#2531 closeout).
11
+ *
8
12
  * When `plan.policy.agentsMdBudget.absoluteMaxBytes` is set, growth past that seeded
9
13
  * ratchet fails closed; distance-to-north-star is always reported. Optional release-gate
10
14
  * north-star enforcement: DEFT_AGENTS_MD_BUDGET_ENFORCE_NORTH_STAR=1 (waiver:
@@ -17,6 +21,9 @@ export type OutputStream = "stdout" | "stderr" | "none";
17
21
  */
18
22
  export declare const ABSOLUTE_MANAGED_MAX_BYTES = 8192;
19
23
  export declare const ABSOLUTE_MANAGED_MAX_TOKENS = 2000;
24
+ /** Phase-3 combined always-on north-star (managed + DD-3 + hooks) — 9 KB (#2531). */
25
+ export declare const COMBINED_ALWAYS_ON_MAX_BYTES = 9216;
26
+ export declare const COMBINED_ALWAYS_ON_MAX_TOKENS = 2304;
20
27
  /** Bootstrap host hooks are 0 B until #2438 ships. */
21
28
  export declare const BOOTSTRAP_HOOK_BYTES = 0;
22
29
  /** Rough UTF-8 bytes-per-token estimate for advisory reporting (~8192 B ≈ ~2048 tok). */
@@ -1,12 +1,17 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { getOpenPackageDefaultInstallTier, resolveOpenPackageTierSkills, } from "../packaging/openpackage-tiers.js";
3
4
  import { AGENTS_MANAGED_CLOSE } from "../platform/constants.js";
4
5
  import { resolveAgentsMdBudget, } from "../policy/agents-md-budget.js";
5
6
  import { measureSkillFrontmatter } from "./skill-frontmatter.js";
6
7
  /**
7
8
  * Layered absolute north-star for the always-on managed surface (#2372 / #2450 / #2452).
8
9
  *
9
- * `ABSOLUTE_MANAGED_MAX_BYTES` is the relocation north-star (<=8192 B / ~2k tok).
10
+ * `ABSOLUTE_MANAGED_MAX_BYTES` is the managed-section relocation north-star (<=8192 B / ~2k tok)
11
+ * — Phase-2 success bar. `COMBINED_ALWAYS_ON_MAX_BYTES` is the Phase-3 combined always-on
12
+ * north-star (managed + DD-3 + hooks), raised to 9 KB after diminishing returns on further
13
+ * thinning (#2531 closeout).
14
+ *
10
15
  * When `plan.policy.agentsMdBudget.absoluteMaxBytes` is set, growth past that seeded
11
16
  * ratchet fails closed; distance-to-north-star is always reported. Optional release-gate
12
17
  * north-star enforcement: DEFT_AGENTS_MD_BUDGET_ENFORCE_NORTH_STAR=1 (waiver:
@@ -19,6 +24,9 @@ import { measureSkillFrontmatter } from "./skill-frontmatter.js";
19
24
  */
20
25
  export const ABSOLUTE_MANAGED_MAX_BYTES = 8192;
21
26
  export const ABSOLUTE_MANAGED_MAX_TOKENS = 2000;
27
+ /** Phase-3 combined always-on north-star (managed + DD-3 + hooks) — 9 KB (#2531). */
28
+ export const COMBINED_ALWAYS_ON_MAX_BYTES = 9216;
29
+ export const COMBINED_ALWAYS_ON_MAX_TOKENS = 2304;
22
30
  /** Bootstrap host hooks are 0 B until #2438 ships. */
23
31
  export const BOOTSTRAP_HOOK_BYTES = 0;
24
32
  /** Rough UTF-8 bytes-per-token estimate for advisory reporting (~8192 B ≈ ~2048 tok). */
@@ -123,12 +131,23 @@ function resolveHarnessProfile(budget, projectRoot) {
123
131
  }
124
132
  return "none";
125
133
  }
126
- function resolveSkillFrontmatterTier(budget) {
134
+ function resolveBootstrapSkillTier(budget, projectRoot) {
127
135
  const env = process.env.DEFT_AGENTS_MD_BUDGET_SKILL_TIER?.trim();
128
136
  if (env === "daily-core" || env === "all" || env === "none") {
129
137
  return env;
130
138
  }
131
- return budget?.skillFrontmatterTier ?? "all";
139
+ if (budget?.skillFrontmatterTier !== undefined) {
140
+ return budget.skillFrontmatterTier;
141
+ }
142
+ try {
143
+ if (getOpenPackageDefaultInstallTier(projectRoot) === "daily-core") {
144
+ return "daily-core";
145
+ }
146
+ }
147
+ catch {
148
+ // No OpenPackage manifest at this project root — fall through.
149
+ }
150
+ return "all";
132
151
  }
133
152
  /** Measure managed + DD-3 skill frontmatter + bootstrap hooks. */
134
153
  export function measureBootstrapSurface(projectRoot, managedText, budget) {
@@ -137,11 +156,19 @@ export function measureBootstrapSurface(projectRoot, managedText, budget) {
137
156
  return managedResult;
138
157
  }
139
158
  const harnessProfile = resolveHarnessProfile(budget, projectRoot);
140
- const tier = resolveSkillFrontmatterTier(budget);
159
+ const tier = resolveBootstrapSkillTier(budget, projectRoot);
160
+ let dailyCoreSkills;
161
+ try {
162
+ dailyCoreSkills = resolveOpenPackageTierSkills(projectRoot, "daily-core");
163
+ }
164
+ catch {
165
+ // Maintainer trees without packaging/openpackage fall back to hardcoded daily-core.
166
+ }
141
167
  const skillFrontmatter = measureSkillFrontmatter(projectRoot, {
142
168
  harnessProfile,
143
169
  tier,
144
170
  bytesPerToken: ABSOLUTE_BYTES_PER_TOKEN_ESTIMATE,
171
+ dailyCoreSkills,
145
172
  });
146
173
  const totalBytes = managedResult.bytes + skillFrontmatter.bytes + BOOTSTRAP_HOOK_BYTES;
147
174
  return {
@@ -166,7 +193,7 @@ function formatNorthStarOverage(measure) {
166
193
  return formatNorthStarOverageBytes(measure.bytes - ABSOLUTE_MANAGED_MAX_BYTES, measure.estimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS);
167
194
  }
168
195
  function formatCombinedNorthStarOverage(bootstrap) {
169
- return formatNorthStarOverageBytes(bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES, bootstrap.totalEstimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS);
196
+ return formatNorthStarOverageBytes(bootstrap.totalBytes - COMBINED_ALWAYS_ON_MAX_BYTES, bootstrap.totalEstimatedTokens - COMBINED_ALWAYS_ON_MAX_TOKENS);
170
197
  }
171
198
  function formatNorthStarDistance(measure) {
172
199
  const overBytes = measure.bytes - ABSOLUTE_MANAGED_MAX_BYTES;
@@ -180,14 +207,14 @@ function formatNorthStarDistance(measure) {
180
207
  `${formatNorthStarOverage(measure)}).`);
181
208
  }
182
209
  function formatCombinedNorthStarDistance(bootstrap) {
183
- const overBytes = bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES;
184
- const overTokens = bootstrap.totalEstimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS;
210
+ const overBytes = bootstrap.totalBytes - COMBINED_ALWAYS_ON_MAX_BYTES;
211
+ const overTokens = bootstrap.totalEstimatedTokens - COMBINED_ALWAYS_ON_MAX_TOKENS;
185
212
  if (overBytes <= 0 && overTokens <= 0) {
186
213
  return (`north-star: combined always-on ${bootstrap.totalBytes} bytes ` +
187
214
  `(~${bootstrap.totalEstimatedTokens} tok) within ` +
188
- `≤${ABSOLUTE_MANAGED_MAX_BYTES} B / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok target.`);
215
+ `≤${COMBINED_ALWAYS_ON_MAX_BYTES} B / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok target.`);
189
216
  }
190
- return (`north-star: combined always-on ≤${ABSOLUTE_MANAGED_MAX_BYTES} B / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
217
+ return (`north-star: combined always-on ≤${COMBINED_ALWAYS_ON_MAX_BYTES} B / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok ` +
191
218
  `(current ${bootstrap.totalBytes} bytes / ~${bootstrap.totalEstimatedTokens} tok — ` +
192
219
  `${formatCombinedNorthStarOverage(bootstrap)}).`);
193
220
  }
@@ -204,12 +231,24 @@ function formatBootstrapItemization(bootstrap) {
204
231
  }
205
232
  function formatAbsoluteAdvisory(bootstrap) {
206
233
  const measure = bootstrap.managed;
234
+ const overManagedBytes = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES;
235
+ const overManagedTokens = measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
236
+ const overCombinedBytes = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES;
237
+ const overCombinedTokens = bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
238
+ const violationLines = [];
239
+ if (overManagedBytes || overManagedTokens) {
240
+ violationLines.push(` Managed section alone: ${measure.bytes} bytes (~${measure.estimatedTokens} tok) exceeds the north-star ` +
241
+ `of ${ABSOLUTE_MANAGED_MAX_BYTES} bytes / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
242
+ `(OVER: ${formatNorthStarOverage(measure)}).`);
243
+ }
244
+ if (overCombinedBytes || overCombinedTokens) {
245
+ violationLines.push(` Combined always-on: ${bootstrap.totalBytes} bytes (~${bootstrap.totalEstimatedTokens} tok) exceeds the north-star ` +
246
+ `of ${COMBINED_ALWAYS_ON_MAX_BYTES} bytes / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok ` +
247
+ `(OVER: ${formatCombinedNorthStarOverage(bootstrap)}).`);
248
+ }
207
249
  return (`⚠ verify:agents-md-budget: always-on bootstrap advisory — ` +
208
250
  `${formatBootstrapItemization(bootstrap)}.\n` +
209
- ` Managed section alone: ${measure.bytes} bytes (~${measure.estimatedTokens} tok) exceeds the north-star ` +
210
- `of ${ABSOLUTE_MANAGED_MAX_BYTES} bytes / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
211
- `(OVER: ${formatNorthStarOverage(measure)}).\n` +
212
- ` ${formatCombinedNorthStarDistance(bootstrap)}\n` +
251
+ `${violationLines.join("\n")}\n` +
213
252
  " Advisory only — set plan.policy.agentsMdBudget.absoluteMaxBytes to enable " +
214
253
  "fail-closed managed growth ratchet (#2452).\n" +
215
254
  " Optional DD-3 ratchet: plan.policy.agentsMdBudget.skillFrontmatterMaxBytes (#2463).\n" +
@@ -248,13 +287,29 @@ function formatSkillFrontmatterRefusal(bootstrap, skillFrontmatterMaxBytes, proj
248
287
  }
249
288
  function formatNorthStarRefusal(bootstrap, projectRoot) {
250
289
  const measure = bootstrap.managed;
251
- const overBytes = bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES;
252
- return (`❌ verify:agents-md-budget: combined always-on surface exceeds the north-star ceiling ` +
290
+ const overNorthStarManaged = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES ||
291
+ measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
292
+ const overNorthStarCombined = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES ||
293
+ bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
294
+ const headline = overNorthStarManaged && overNorthStarCombined
295
+ ? "managed and combined always-on surfaces exceed the north-star ceiling"
296
+ : overNorthStarManaged
297
+ ? "managed section exceeds the north-star ceiling"
298
+ : "combined always-on surface exceeds the north-star ceiling";
299
+ const detailLines = [];
300
+ if (overNorthStarManaged) {
301
+ detailLines.push(` managed section: ${measure.bytes}/${ABSOLUTE_MANAGED_MAX_BYTES} bytes ` +
302
+ `(OVER: ${formatNorthStarOverage(measure)})`);
303
+ }
304
+ if (overNorthStarCombined) {
305
+ detailLines.push(` combined always-on: ${bootstrap.totalBytes}/${COMBINED_ALWAYS_ON_MAX_BYTES} bytes ` +
306
+ `(OVER: ${formatCombinedNorthStarOverage(bootstrap)})`);
307
+ }
308
+ return (`❌ verify:agents-md-budget: ${headline} ` +
253
309
  `(project_root=${projectRoot}, release-gate mode).\n` +
254
310
  ` ${formatBootstrapItemization(bootstrap)}\n` +
255
- ` managed section: ${measure.bytes}/${ABSOLUTE_MANAGED_MAX_BYTES} bytes\n` +
256
- ` combined OVER by ${overBytes} bytes vs north-star\n` +
257
- " Thin the managed section and/or tier DD-3 skills toward the <=8192 B target,\n" +
311
+ `${detailLines.join("\n")}\n` +
312
+ " Thin the managed section and/or tier DD-3 skills toward the combined ≤9 KB target,\n" +
258
313
  " or set DEFT_ALLOW_ABSOLUTE_BUDGET_WAIVER=1 for a time-boxed operator waiver (#2452).");
259
314
  }
260
315
  function enforceNorthStarEnabled() {
@@ -267,8 +322,8 @@ function attachNorthStarNote(result, bootstrap, options) {
267
322
  const measure = bootstrap.managed;
268
323
  const overManagedBytes = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES;
269
324
  const overManagedTokens = measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
270
- const overCombinedBytes = bootstrap.totalBytes > ABSOLUTE_MANAGED_MAX_BYTES;
271
- const overCombinedTokens = bootstrap.totalEstimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
325
+ const overCombinedBytes = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES;
326
+ const overCombinedTokens = bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
272
327
  if (options.advisoryOnly) {
273
328
  if (!overManagedBytes && !overManagedTokens && !overCombinedBytes && !overCombinedTokens) {
274
329
  return result;
@@ -425,8 +480,8 @@ export function evaluate(projectRoot, options = {}) {
425
480
  }
426
481
  const overNorthStarManaged = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES ||
427
482
  measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
428
- const overNorthStarCombined = bootstrap.totalBytes > ABSOLUTE_MANAGED_MAX_BYTES ||
429
- bootstrap.totalEstimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
483
+ const overNorthStarCombined = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES ||
484
+ bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
430
485
  if (enforceNorthStarEnabled() &&
431
486
  (overNorthStarManaged || overNorthStarCombined) &&
432
487
  !northStarWaiverActive()) {
@@ -27,6 +27,8 @@ export interface MeasureSkillFrontmatterOptions {
27
27
  readonly tier?: SkillFrontmatterTier;
28
28
  readonly harnessProfile?: HarnessProfile;
29
29
  readonly bytesPerToken?: number;
30
+ /** Override daily-core skill names (from OpenPackage resolveTierSkills). */
31
+ readonly dailyCoreSkills?: readonly string[];
30
32
  }
31
33
  /** Measure harness-injected skill frontmatter bytes for the configured profile/tier. */
32
34
  export declare function measureSkillFrontmatter(projectRoot: string, options?: MeasureSkillFrontmatterOptions): SkillFrontmatterMeasure;
@@ -61,14 +61,14 @@ function listSkillDirs(skillsRoot) {
61
61
  .map((entry) => entry.name)
62
62
  .sort();
63
63
  }
64
- function skillMatchesTier(skillName, tier) {
64
+ function skillInSelectedTier(skillName, tier, dailyCore) {
65
65
  if (tier === "none") {
66
66
  return false;
67
67
  }
68
68
  if (tier === "all") {
69
69
  return true;
70
70
  }
71
- return DAILY_CORE_SET.has(skillName);
71
+ return dailyCore.has(skillName);
72
72
  }
73
73
  /** Measure harness-injected skill frontmatter bytes for the configured profile/tier. */
74
74
  export function measureSkillFrontmatter(projectRoot, options = {}) {
@@ -76,6 +76,7 @@ export function measureSkillFrontmatter(projectRoot, options = {}) {
76
76
  const tier = options.tier ?? "all";
77
77
  const bytesPerToken = options.bytesPerToken ?? 4;
78
78
  const skillsRoot = options.skillsRoot ?? defaultSkillsRoot(projectRoot);
79
+ const dailyCore = options.dailyCoreSkills !== undefined ? new Set(options.dailyCoreSkills) : DAILY_CORE_SET;
79
80
  if (harnessProfile === "none" || tier === "none") {
80
81
  return {
81
82
  bytes: 0,
@@ -88,7 +89,7 @@ export function measureSkillFrontmatter(projectRoot, options = {}) {
88
89
  }
89
90
  const entries = [];
90
91
  for (const skillName of listSkillDirs(skillsRoot)) {
91
- if (!skillMatchesTier(skillName, tier)) {
92
+ if (!skillInSelectedTier(skillName, tier, dailyCore)) {
92
93
  continue;
93
94
  }
94
95
  const skillPath = join(skillsRoot, skillName, "SKILL.md");
@@ -1,7 +1,9 @@
1
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
1
2
  import { type CacheCaps, type EntryUsage } from "./quota.js";
2
3
  import { SCANNER_VERSION } from "./scanner.js";
3
4
  import { type Clock } from "./time.js";
4
5
  import type { CacheGetOptions, CachePutOptions, GetResult, PutResult } from "./types.js";
6
+ export { ProjectionContainmentError as CacheContainmentError };
5
7
  /** Write a cache entry (mirrors `cache.cache_put`). */
6
8
  export declare function cachePut(source: string, key: string, raw: Record<string, unknown>, options?: CachePutOptions): PutResult;
7
9
  /** Read a cache entry (mirrors `cache.cache_get`). */
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
3
4
  import { pyRepr } from "../scm/py-format.js";
4
5
  import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
5
6
  import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
@@ -10,6 +11,15 @@ import { enforceCaps, predictEvictionSet, resolveCaps, } from "./quota.js";
10
11
  import { flagsForMeta, SCANNER_VERSION, scan } from "./scanner.js";
11
12
  import { addSeconds, parseIso, systemClock, utcIso } from "./time.js";
12
13
  import { validateMeta } from "./validate.js";
14
+ export { ProjectionContainmentError as CacheContainmentError };
15
+ /** Refuse symlink-escaping `.deft-cache` before mkdir/read/write (#2470). */
16
+ function assertWritableCachePath(cacheRoot, ...segments) {
17
+ const cacheAbs = resolve(cacheRoot);
18
+ const projectDir = dirname(cacheAbs);
19
+ const target = segments.length > 0 ? join(cacheAbs, ...segments) : cacheAbs;
20
+ assertProjectionContained(projectDir, target);
21
+ return target;
22
+ }
13
23
  function renderContent(source, raw) {
14
24
  if (source === "github-issue") {
15
25
  const number = raw.number;
@@ -82,6 +92,7 @@ export function cachePut(source, key, raw, options = {}) {
82
92
  }
83
93
  const expires = addSeconds(fetched, ttl);
84
94
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
95
+ assertWritableCachePath(cacheRoot);
85
96
  const edir = entryDir(source, key, cacheRoot);
86
97
  const rawText = pythonJsonDump(raw);
87
98
  const rawSize = Buffer.byteLength(rawText, "utf8");
@@ -182,6 +193,7 @@ export function cachePut(source, key, raw, options = {}) {
182
193
  export function cacheGet(source, key, options = {}) {
183
194
  const clock = options.clock ?? systemClock;
184
195
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
196
+ assertWritableCachePath(cacheRoot);
185
197
  const allowStale = options.allowStale ?? true;
186
198
  const edir = entryDir(source, key, cacheRoot);
187
199
  const metaRelPath = `${source}/${key}/meta.json`;
@@ -220,6 +232,7 @@ export function cacheInvalidate(source, key, options = {}) {
220
232
  const clock = options.clock ?? systemClock;
221
233
  validateKey(source, key);
222
234
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
235
+ assertWritableCachePath(cacheRoot);
223
236
  const edir = entryDir(source, key, cacheRoot);
224
237
  const existed = existsSync(edir);
225
238
  if (existed)
@@ -252,6 +265,7 @@ export function cachePrune(options = {}) {
252
265
  throw new CacheError(`--older-than-days must be >= 0 (got ${JSON.stringify(olderThanDays)})`);
253
266
  }
254
267
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
268
+ assertWritableCachePath(cacheRoot);
255
269
  if (!existsSync(cacheRoot))
256
270
  return [];
257
271
  const cutoff = addSeconds(clock.now(), -olderThanDays * 24 * 60 * 60);
@@ -313,6 +327,7 @@ function collectMetaPathsUnder(srcRoot) {
313
327
  export function cachePruneToCap(options = {}) {
314
328
  const clock = options.clock ?? systemClock;
315
329
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
330
+ assertWritableCachePath(cacheRoot);
316
331
  const resolved = options.caps ?? resolveCaps();
317
332
  if (!resolved.maxBytes && !resolved.maxEntries)
318
333
  return [];
@@ -11,6 +11,10 @@ const EXCLUDED_PARTS = new Set([
11
11
  "node_modules",
12
12
  "__pycache__",
13
13
  ".pytest_cache",
14
+ ".deft-scratch",
15
+ ".deft-cache",
16
+ "coverage",
17
+ "dist",
14
18
  ]);
15
19
  function frontmatter(text) {
16
20
  if (!text.startsWith("---")) {
@@ -29,13 +29,13 @@ export const DOCTOR_ALLOWED_FLAGS = [
29
29
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
30
30
  export const NPM_PACKAGE_NAME = "@deftai/directive";
31
31
  // #2182: payload-staleness is the only doctor check that can reach a network
32
- // endpoint (git ls-remote against the framework's git remote, falling back to
33
- // `npm view` against the npm registry). It is OFF by default (offline tier)
32
+ // endpoint (git ls-remote verifies the pinned ref and `npm view` compares a
33
+ // release-tag install with the latest stable package). It is OFF by default (offline tier)
34
34
  // and requires the explicit `--network` flag; this line discloses exactly
35
35
  // which tool + registry class it may contact BEFORE the check runs, and the
36
36
  // skip line tells an offline run how to opt in.
37
37
  export const NETWORK_DISCLOSURE_LINE = "[deft doctor] --network: this run may contact your git remote (framework " +
38
- "repo host) and, as a fallback, the npm registry (registry.npmjs.org) to " +
38
+ "repo host) and the npm registry (registry.npmjs.org) to " +
39
39
  `look up the latest ${NPM_PACKAGE_NAME} version.`;
40
40
  export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (default). Run `deft doctor --network` to check " +
41
41
  "framework currency against your git remote and the npm registry " +
@@ -8,6 +8,7 @@ export * from "./main.js";
8
8
  export * from "./manifest.js";
9
9
  export * from "./paths.js";
10
10
  export * from "./payload-staleness.js";
11
+ export * from "./release-availability.js";
11
12
  export * from "./taskfile.js";
12
13
  export * from "./types.js";
13
14
  export * from "./which.js";
@@ -8,6 +8,7 @@ export * from "./main.js";
8
8
  export * from "./manifest.js";
9
9
  export * from "./paths.js";
10
10
  export * from "./payload-staleness.js";
11
+ export * from "./release-availability.js";
11
12
  export * from "./taskfile.js";
12
13
  export * from "./types.js";
13
14
  export * from "./which.js";
@@ -163,7 +163,8 @@ export function cmdDoctor(args, seams = {}) {
163
163
  sink.blank();
164
164
  }
165
165
  // #2182: payload-staleness is the only doctor check that can reach a
166
- // registry (git remote, then npm as a fallback). It stays in the OFFLINE
166
+ // registry (git verifies the pin; npm compares stable release availability).
167
+ // It stays in the OFFLINE
167
168
  // tier (skipped) unless the operator explicitly opts into the NETWORK tier
168
169
  // via `--network`, and the tool + registry class is disclosed BEFORE the
169
170
  // check runs -- never silently, never as a side effect of a read-only
@@ -4,6 +4,7 @@ import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/m
4
4
  import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
5
5
  import { locateManifest, parseInstallManifest } from "./manifest.js";
6
6
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
7
+ import { evaluateReleaseAvailability } from "./release-availability.js";
7
8
  function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
8
9
  try {
9
10
  const agents = join(projectRoot, "AGENTS.md");
@@ -38,42 +39,6 @@ function parseRemoteSha(stdout) {
38
39
  const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
39
40
  return firstLine.trim().split(/\s+/)[0] ?? "";
40
41
  }
41
- function parseSemver(version) {
42
- const normalized = version.trim().replace(/^v/i, "");
43
- const parts = [];
44
- for (const segment of normalized.split(".")) {
45
- const numeric = Number.parseInt(segment.split("-")[0] ?? "", 10);
46
- if (Number.isNaN(numeric)) {
47
- break;
48
- }
49
- parts.push(numeric);
50
- }
51
- return parts.length > 0 ? parts : [0];
52
- }
53
- function semverLessThan(left, right) {
54
- const a = parseSemver(left);
55
- const b = parseSemver(right);
56
- const len = Math.max(a.length, b.length);
57
- for (let i = 0; i < len; i += 1) {
58
- const av = a[i] ?? 0;
59
- const bv = b[i] ?? 0;
60
- if (av < bv) {
61
- return true;
62
- }
63
- if (av > bv) {
64
- return false;
65
- }
66
- }
67
- return false;
68
- }
69
- function manifestVersion(ref, tag) {
70
- const candidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
71
- const normalized = candidate.replace(/^v/i, "");
72
- if (!/^\d+(?:\.\d+)*/.test(normalized)) {
73
- return "";
74
- }
75
- return normalized;
76
- }
77
42
  function defaultNpmViewVersion() {
78
43
  const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
79
44
  encoding: "utf8",
@@ -82,6 +47,25 @@ function defaultNpmViewVersion() {
82
47
  const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
83
48
  return { ok: proc.status === 0 && version.length > 0, version };
84
49
  }
50
+ function emitReleaseAvailable(checkName, installedVersion, latestVersion, ref, upgradeCommand, sink, addFinding) {
51
+ const msg = `Newer framework release available (installed v${installedVersion}; ` +
52
+ `latest v${latestVersion} from npm registry). ` +
53
+ `Recommendation: run \`${upgradeCommand}\`.`;
54
+ sink.warn(msg);
55
+ addFinding({
56
+ severity: "warning",
57
+ message: msg,
58
+ check: checkName,
59
+ status: "stale",
60
+ staleness_kind: "newer-release",
61
+ ref,
62
+ installed_version: installedVersion,
63
+ latest_version: latestVersion,
64
+ remote_version: latestVersion,
65
+ resolver: "npm-view",
66
+ suggestion: upgradeCommand,
67
+ });
68
+ }
85
69
  function emitUnverified(checkName, reason, sink, addFinding) {
86
70
  const msg = `payload currency UNVERIFIED — ${reason}`;
87
71
  sink.warn(msg);
@@ -101,8 +85,8 @@ function resolveUpgradeCommand(projectRoot, manifest, isFile, pm = "npm") {
101
85
  }
102
86
  return vendored ? VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND : CANONICAL_UPGRADE_COMMAND;
103
87
  }
104
- function emitStale(checkName, installedLabel, remoteLabel, ref, upgradeCommand, sink, addFinding, extras = {}, behindWord = "remote") {
105
- const msg = `Framework payload is stale (installed ${installedLabel} behind ${behindWord} ${remoteLabel} for ref '${ref}'). ` +
88
+ function emitStale(checkName, installedLabel, remoteLabel, ref, upgradeCommand, sink, addFinding, extras = {}) {
89
+ const msg = `Framework payload is stale (installed ${installedLabel} behind remote ${remoteLabel} for ref '${ref}'). ` +
106
90
  `Recommendation: run \`${upgradeCommand}\` from any shell with Node ≥ 20.`;
107
91
  sink.warn(msg);
108
92
  addFinding({
@@ -209,37 +193,52 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
209
193
  catch {
210
194
  remoteResult = { ok: false, stdout: "" };
211
195
  }
212
- if (remoteResult.ok) {
213
- const remoteSha = parseRemoteSha(remoteResult.stdout);
214
- if (remoteSha) {
215
- if (installedSha === remoteSha) {
216
- sink.info(`${checkName}: current (sha matches remote)`);
217
- return;
218
- }
219
- emitStale(checkName, `sha ${installedSha.slice(0, 8)}...`, `sha ${remoteSha.slice(0, 8)}...`, ref, upgradeCommand, sink, addFinding, { installed_sha: installedSha, remote_sha: remoteSha, resolver: "git-ls-remote" });
220
- return;
221
- }
196
+ const remoteSha = remoteResult.ok ? parseRemoteSha(remoteResult.stdout) : "";
197
+ if (remoteSha && installedSha !== remoteSha) {
198
+ emitStale(checkName, `sha ${installedSha.slice(0, 8)}...`, `sha ${remoteSha.slice(0, 8)}...`, ref, upgradeCommand, sink, addFinding, {
199
+ installed_sha: installedSha,
200
+ remote_sha: remoteSha,
201
+ resolver: "git-ls-remote",
202
+ staleness_kind: "pinned-ref-moved",
203
+ });
204
+ return;
222
205
  }
223
- const npmResult = runNpmView();
224
- const installedVersion = manifestVersion(ref, tag);
225
- if (npmResult.ok && installedVersion) {
226
- if (semverLessThan(installedVersion, npmResult.version)) {
227
- emitStale(checkName, `v${installedVersion}`, `v${npmResult.version}`, ref, upgradeCommand, sink, addFinding, {
228
- installed_version: installedVersion,
229
- remote_version: npmResult.version,
230
- resolver: "npm-view",
231
- }, "npm registry");
232
- return;
233
- }
234
- if (installedVersion === npmResult.version.replace(/^v/i, "")) {
235
- sink.info(`${checkName}: current (version matches npm registry)`);
206
+ const installedCandidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
207
+ const applicability = evaluateReleaseAvailability(installedCandidate, null);
208
+ if (applicability.status === "not-applicable") {
209
+ if (remoteSha && installedSha === remoteSha) {
210
+ sink.info(`${checkName}: current (sha matches remote; ref is not a release tag)`);
236
211
  return;
237
212
  }
238
- sink.info(`${checkName}: current (installed version >= npm registry)`);
213
+ const reason = remoteResult.ok
214
+ ? "ls-remote produced no sha; ref is not a release tag"
215
+ : "could not reach remote; ref is not a release tag";
216
+ sink.info(`${checkName}: skip -- ${reason}`);
217
+ emitUnverified(checkName, reason, sink, addFinding);
218
+ return;
219
+ }
220
+ let npmResult;
221
+ try {
222
+ npmResult = runNpmView();
223
+ }
224
+ catch {
225
+ npmResult = { ok: false, version: "" };
226
+ }
227
+ const availability = evaluateReleaseAvailability(installedCandidate, npmResult.ok ? npmResult.version : null);
228
+ if (availability.status === "available") {
229
+ emitReleaseAvailable(checkName, availability.installedVersion, availability.latestVersion, ref, upgradeCommand, sink, addFinding);
230
+ return;
231
+ }
232
+ if (availability.status === "current") {
233
+ sink.info(`${checkName}: current (installed release >= npm latest)`);
234
+ return;
235
+ }
236
+ if (availability.status === "prerelease-ignored") {
237
+ sink.info(`${checkName}: current (npm candidate is a prerelease; stable install retained)`);
239
238
  return;
240
239
  }
241
240
  const reason = remoteResult.ok
242
- ? "ls-remote produced no sha and npm registry fallback unavailable"
241
+ ? "npm registry release lookup unavailable or returned a non-publishable version"
243
242
  : "could not reach remote (git ls-remote / npm view both unavailable)";
244
243
  sink.info(`${checkName}: skip -- ${reason}`);
245
244
  emitUnverified(checkName, reason, sink, addFinding);
@@ -0,0 +1,28 @@
1
+ /** Outcomes produced by the reusable latest-release comparison. */
2
+ export type ReleaseAvailabilityStatus = "available" | "current" | "prerelease-ignored" | "not-applicable" | "unverified";
3
+ /** Network resolver that supplied the latest published version. */
4
+ export type ReleaseAvailabilityResolver = "npm-view";
5
+ /** Typed result consumed by doctor today and safe-idle upgrade prompting later. */
6
+ export type ReleaseAvailabilityResult = {
7
+ readonly status: "available" | "current" | "prerelease-ignored";
8
+ readonly installedVersion: string;
9
+ readonly latestVersion: string;
10
+ readonly resolver: ReleaseAvailabilityResolver;
11
+ } | {
12
+ readonly status: "not-applicable";
13
+ readonly installedVersion: null;
14
+ readonly latestVersion: string | null;
15
+ readonly resolver: ReleaseAvailabilityResolver;
16
+ } | {
17
+ readonly status: "unverified";
18
+ readonly installedVersion: string;
19
+ readonly latestVersion: null;
20
+ readonly resolver: ReleaseAvailabilityResolver;
21
+ };
22
+ /**
23
+ * Compare an installed release with a resolver-provided latest release.
24
+ * Stable installs deliberately ignore prerelease candidates even when their
25
+ * numeric core is newer.
26
+ */
27
+ export declare function evaluateReleaseAvailability(installed: string, latest: string | null, resolver?: ReleaseAvailabilityResolver): ReleaseAvailabilityResult;
28
+ //# sourceMappingURL=release-availability.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { comparePublishableVersions, isPrereleaseTag, isPublishable } from "../release/version.js";
2
+ function normalizePublishableVersion(version) {
3
+ if (version === null)
4
+ return null;
5
+ const candidate = version.trim().replace(/^refs\/tags\//, "");
6
+ if (!candidate || !isPublishable(candidate))
7
+ return null;
8
+ return candidate.replace(/^v/, "");
9
+ }
10
+ /**
11
+ * Compare an installed release with a resolver-provided latest release.
12
+ * Stable installs deliberately ignore prerelease candidates even when their
13
+ * numeric core is newer.
14
+ */
15
+ export function evaluateReleaseAvailability(installed, latest, resolver = "npm-view") {
16
+ const installedVersion = normalizePublishableVersion(installed);
17
+ const latestVersion = normalizePublishableVersion(latest);
18
+ if (installedVersion === null) {
19
+ return { status: "not-applicable", installedVersion, latestVersion, resolver };
20
+ }
21
+ if (latestVersion === null) {
22
+ return { status: "unverified", installedVersion, latestVersion, resolver };
23
+ }
24
+ if (!isPrereleaseTag(installedVersion) && isPrereleaseTag(latestVersion)) {
25
+ return { status: "prerelease-ignored", installedVersion, latestVersion, resolver };
26
+ }
27
+ const comparison = comparePublishableVersions(installedVersion, latestVersion);
28
+ return {
29
+ status: comparison < 0 ? "available" : "current",
30
+ installedVersion,
31
+ latestVersion,
32
+ resolver,
33
+ };
34
+ }
35
+ //# sourceMappingURL=release-availability.js.map
@@ -1,4 +1,5 @@
1
- export declare const CRUD_METRICS_HISTORY_REL = "results/crud-metrics.jsonl";
1
+ /** Relative path under the resolved metrics home for helped / value telemetry (#2545). */
2
+ export declare const CRUD_METRICS_HISTORY_REL = "helped/crud-metrics.jsonl";
2
3
  export type CrudOperation = "create" | "read" | "update" | "delete";
3
4
  export type ByteDiffMinimality = "surgical" | "whole-file-rewrite";
4
5
  export interface CrudOperationMetric {
@@ -31,9 +32,9 @@ export declare function classifyByteDiffMinimality(before: string, after: string
31
32
  readonly kind: ByteDiffMinimality;
32
33
  readonly changedRatio: number;
33
34
  };
34
- /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1). */
35
- export declare function crudMetricsHistoryPath(projectRoot: string): string;
36
- /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1). */
35
+ /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1 / #2545). */
36
+ export declare function crudMetricsHistoryPath(projectRoot: string): string | null;
37
+ /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1 / #2545). */
37
38
  export declare function persistCrudMetrics(projectRoot: string, metrics: readonly CrudOperationMetric[]): void;
38
39
  /**
39
40
  * Instrumented vBRIEF/xBRIEF CRUD chokepoint (#1703 Tier 1).