@deftai/directive-core 0.55.1 → 0.56.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/cache/errors.d.ts +4 -1
  2. package/dist/cache/errors.js +8 -2
  3. package/dist/cache/fetch.d.ts +50 -0
  4. package/dist/cache/fetch.js +224 -3
  5. package/dist/cache/main.js +10 -5
  6. package/dist/cache/operations.js +6 -4
  7. package/dist/codebase/map-fresh.js +7 -1
  8. package/dist/deposit/copy-tree.d.ts +16 -0
  9. package/dist/deposit/copy-tree.js +58 -0
  10. package/dist/deposit/resolve-content.d.ts +28 -0
  11. package/dist/deposit/resolve-content.js +70 -0
  12. package/dist/doctor/checks.d.ts +6 -0
  13. package/dist/doctor/checks.js +37 -0
  14. package/dist/doctor/constants.d.ts +2 -0
  15. package/dist/doctor/constants.js +8 -0
  16. package/dist/engine-version.d.ts +3 -0
  17. package/dist/engine-version.js +23 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +3 -1
  20. package/dist/init-deposit/gitignore.d.ts +51 -0
  21. package/dist/init-deposit/gitignore.js +200 -0
  22. package/dist/init-deposit/index.d.ts +6 -0
  23. package/dist/init-deposit/index.js +6 -0
  24. package/dist/init-deposit/init-deposit.d.ts +44 -0
  25. package/dist/init-deposit/init-deposit.js +197 -0
  26. package/dist/init-deposit/legacy-detect.d.ts +61 -0
  27. package/dist/init-deposit/legacy-detect.js +223 -0
  28. package/dist/init-deposit/migrate.d.ts +88 -0
  29. package/dist/init-deposit/migrate.js +196 -0
  30. package/dist/init-deposit/refresh.d.ts +50 -0
  31. package/dist/init-deposit/refresh.js +292 -0
  32. package/dist/init-deposit/scaffold.d.ts +41 -0
  33. package/dist/init-deposit/scaffold.js +753 -0
  34. package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
  35. package/dist/legacy-bridge/bridge-drift.js +171 -0
  36. package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
  37. package/dist/legacy-bridge/freeze-gate.js +168 -0
  38. package/dist/legacy-bridge/index.d.ts +4 -0
  39. package/dist/legacy-bridge/index.js +4 -0
  40. package/dist/legacy-bridge/sot.d.ts +45 -0
  41. package/dist/legacy-bridge/sot.js +49 -0
  42. package/dist/preflight-cache/evaluate.d.ts +11 -0
  43. package/dist/preflight-cache/evaluate.js +63 -13
  44. package/dist/release-e2e/constants.d.ts +2 -0
  45. package/dist/release-e2e/constants.js +2 -0
  46. package/dist/release-e2e/flags.js +5 -1
  47. package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
  48. package/dist/release-e2e/legacy-bridge-leg.js +460 -0
  49. package/dist/release-e2e/main.d.ts +2 -1
  50. package/dist/release-e2e/main.js +17 -1
  51. package/dist/release-e2e/npm-ops.d.ts +3 -1
  52. package/dist/release-e2e/npm-ops.js +5 -3
  53. package/dist/release-e2e/types.d.ts +9 -0
  54. package/dist/render/index.d.ts +1 -1
  55. package/dist/render/index.js +1 -1
  56. package/dist/render/project-render.js +4 -3
  57. package/dist/render/roadmap-render.d.ts +4 -2
  58. package/dist/render/roadmap-render.js +14 -7
  59. package/dist/swarm/routing-verify.js +2 -2
  60. package/dist/swarm/subagent-backend.d.ts +16 -0
  61. package/dist/swarm/subagent-backend.js +14 -0
  62. package/dist/triage/actions/candidates-log.d.ts +12 -1
  63. package/dist/triage/actions/candidates-log.js +44 -7
  64. package/dist/triage/actions/index.d.ts +25 -1
  65. package/dist/triage/actions/index.js +94 -3
  66. package/dist/triage/summary/index.d.ts +1 -3
  67. package/dist/triage/summary/index.js +6 -57
  68. package/dist/triage/summary/reconcilable.js +4 -33
  69. package/dist/triage/welcome/default-mode.d.ts +1 -0
  70. package/dist/triage/welcome/default-mode.js +6 -0
  71. package/dist/vbrief-validate/project-definition.js +4 -69
  72. package/dist/vbrief-validate/registry-status.d.ts +20 -0
  73. package/dist/vbrief-validate/registry-status.js +85 -0
  74. package/package.json +11 -2
@@ -2,9 +2,12 @@
2
2
  export declare class CacheError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
+ /** Format like Python ``KeyError`` str() for golden parity with the oracle. */
6
+ export declare function keyErrorMessage(inner: string): string;
5
7
  /** Cache miss for the requested (source, key). */
6
8
  export declare class CacheNotFoundError extends Error {
7
- constructor(message: string);
9
+ readonly innerMessage: string;
10
+ constructor(inner: string);
8
11
  }
9
12
  /** meta.json failed schema validation on read or write. */
10
13
  export declare class CacheValidationError extends Error {
@@ -5,10 +5,16 @@ export class CacheError extends Error {
5
5
  this.name = "CacheError";
6
6
  }
7
7
  }
8
+ /** Format like Python ``KeyError`` str() for golden parity with the oracle. */
9
+ export function keyErrorMessage(inner) {
10
+ return `"${inner.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11
+ }
8
12
  /** Cache miss for the requested (source, key). */
9
13
  export class CacheNotFoundError extends Error {
10
- constructor(message) {
11
- super(message);
14
+ innerMessage;
15
+ constructor(inner) {
16
+ super(keyErrorMessage(inner));
17
+ this.innerMessage = inner;
12
18
  this.name = "CacheNotFoundError";
13
19
  }
14
20
  }
@@ -51,6 +51,9 @@ export declare function runFetchAll(options: {
51
51
  limit?: number;
52
52
  labels?: readonly string[];
53
53
  author?: string | null;
54
+ force?: boolean;
55
+ contentDriftNumbers?: readonly number[];
56
+ probeDriftFn?: typeof probeCacheDrift;
54
57
  isFresh?: (metaPath: string) => boolean;
55
58
  doPut?: (key: string, raw: Record<string, unknown>) => void;
56
59
  }): FetchAllReportImpl;
@@ -65,6 +68,9 @@ export declare function cacheFetchAll(options: {
65
68
  labels?: readonly string[];
66
69
  author?: string | null;
67
70
  cacheRoot?: string;
71
+ force?: boolean;
72
+ contentDriftNumbers?: readonly number[];
73
+ probeDriftFn?: typeof probeCacheDrift;
68
74
  }): FetchAllReportImpl;
69
75
  export declare class StateRefreshReportImpl implements StateRefreshReport {
70
76
  revisited: number;
@@ -97,5 +103,49 @@ export declare function cacheRefreshClosed(options: {
97
103
  delayMs?: number;
98
104
  limit?: number;
99
105
  cacheRoot?: string;
106
+ openNumbers?: Set<number>;
107
+ listOpenFn?: (repo: string, limit: number) => Set<number>;
100
108
  }): StateRefreshReportImpl;
109
+ export declare const DEFAULT_SELF_HEAL_TTL_SECONDS = 3600;
110
+ export declare const SELF_HEAL_STATE_FILENAME = "self-heal-state.json";
111
+ export declare const DEFAULT_MAX_CONTENT_DRIFT_CHECKS = 25;
112
+ export interface CacheDriftProbeResult {
113
+ readonly stateDriftNumbers: readonly number[];
114
+ readonly contentDriftNumbers: readonly number[];
115
+ }
116
+ /** Diff cached-open issue numbers vs the live open set and TTL-fresh content drift. */
117
+ export declare function probeCacheDrift(options: {
118
+ repo: string;
119
+ source?: string;
120
+ cacheRoot?: string;
121
+ limit?: number;
122
+ includeContentDrift?: boolean;
123
+ maxContentDriftChecks?: number;
124
+ listOpenFn?: (repo: string, limit: number) => Set<number>;
125
+ fetchSingleFn?: (repo: string, n: number) => Record<string, unknown>;
126
+ isFreshFn?: (metaPath: string) => boolean;
127
+ }): CacheDriftProbeResult;
128
+ export interface SelfHealResult {
129
+ readonly skipped: boolean;
130
+ readonly skipReason: string | null;
131
+ readonly drift: CacheDriftProbeResult | null;
132
+ readonly refresh: StateRefreshReportImpl | null;
133
+ }
134
+ /** TTL-bounded closed-reconcile for session ritual / triage:welcome self-healing (#1886). */
135
+ export declare function maybeSelfHealCache(projectRoot: string, options?: {
136
+ repo?: string | null;
137
+ source?: string;
138
+ cacheRoot?: string;
139
+ ttlSeconds?: number;
140
+ nowFn?: () => Date;
141
+ listOpenFn?: (repo: string, limit: number) => Set<number>;
142
+ fetchSingleFn?: (repo: string, n: number) => Record<string, unknown>;
143
+ refreshFn?: (opts: {
144
+ source: string;
145
+ repo: string;
146
+ cacheRoot: string;
147
+ openNumbers: Set<number>;
148
+ }) => StateRefreshReportImpl;
149
+ writeState?: boolean;
150
+ }): SelfHealResult;
101
151
  //# sourceMappingURL=fetch.d.ts.map
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
4
4
  import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
@@ -198,6 +198,31 @@ function listIssuesRest(repo, options) {
198
198
  throw err;
199
199
  }
200
200
  }
201
+ function resolveContentDriftNumbers(options) {
202
+ if (options.force)
203
+ return new Set();
204
+ if (options.contentDriftNumbers !== undefined) {
205
+ return new Set(options.contentDriftNumbers);
206
+ }
207
+ try {
208
+ const probeOpts = {
209
+ repo: options.repo,
210
+ source: options.source,
211
+ cacheRoot: options.cacheRoot,
212
+ };
213
+ if (options.fetchSingleFn)
214
+ probeOpts.fetchSingleFn = options.fetchSingleFn;
215
+ const listOpenNumbers = options.listOpenNumbers;
216
+ if (listOpenNumbers) {
217
+ probeOpts.listOpenFn = () => listOpenNumbers;
218
+ }
219
+ const drift = (options.probeDriftFn ?? probeCacheDrift)(probeOpts);
220
+ return new Set(drift.contentDriftNumbers);
221
+ }
222
+ catch {
223
+ return new Set();
224
+ }
225
+ }
201
226
  export function runFetchAll(options) {
202
227
  const issues = listIssuesRest(options.repo, {
203
228
  state: options.state ?? "open",
@@ -205,6 +230,13 @@ export function runFetchAll(options) {
205
230
  labels: options.labels ?? [],
206
231
  author: options.author ?? null,
207
232
  });
233
+ const issueByNumber = new Map();
234
+ for (const issue of issues) {
235
+ const number = issue.number;
236
+ if (typeof number === "number" && number > 0) {
237
+ issueByNumber.set(number, issue);
238
+ }
239
+ }
208
240
  const report = new FetchAllReportImpl();
209
241
  const total = issues.length;
210
242
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
@@ -217,6 +249,21 @@ export function runFetchAll(options) {
217
249
  ((key, raw) => {
218
250
  cachePut(source, key, raw, { ttlSeconds, cacheRoot });
219
251
  });
252
+ const contentDriftNumbers = resolveContentDriftNumbers({
253
+ force: options.force,
254
+ repo: options.repo,
255
+ source,
256
+ cacheRoot,
257
+ contentDriftNumbers: options.contentDriftNumbers,
258
+ probeDriftFn: options.probeDriftFn,
259
+ fetchSingleFn: (repo, n) => {
260
+ const fromList = issueByNumber.get(n);
261
+ if (fromList !== undefined)
262
+ return normaliseRestIssue(fromList);
263
+ return singleIssueFetcherImpl(repo, n);
264
+ },
265
+ listOpenNumbers: new Set(issueByNumber.keys()),
266
+ });
220
267
  if (total >= PROGRESS_EVERY_N) {
221
268
  emitFetchProgress(options.repo, "enumerated", 0, total, report);
222
269
  }
@@ -234,7 +281,9 @@ export function runFetchAll(options) {
234
281
  else {
235
282
  const key = `${options.repo}/${number}`;
236
283
  const edir = entryDir(source, key, cacheRoot);
237
- if (isFreshFn(join(edir, "meta.json"))) {
284
+ const metaPath = join(edir, "meta.json");
285
+ const skipFresh = !options.force && isFreshFn(metaPath) && !contentDriftNumbers.has(number);
286
+ if (skipFresh) {
238
287
  report.alreadyFresh += 1;
239
288
  }
240
289
  else {
@@ -284,6 +333,9 @@ export function cacheFetchAll(options) {
284
333
  limit: options.limit,
285
334
  labels: options.labels,
286
335
  author: options.author,
336
+ force: options.force,
337
+ contentDriftNumbers: options.contentDriftNumbers,
338
+ probeDriftFn: options.probeDriftFn,
287
339
  });
288
340
  }
289
341
  export class StateRefreshReportImpl {
@@ -395,7 +447,9 @@ export function scanCachedOpenEntries(repo, source, cacheRoot) {
395
447
  export function cacheRefreshClosed(options) {
396
448
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
397
449
  const cachedOpen = scanCachedOpenEntries(options.repo, options.source, cacheRoot);
398
- const openNumbers = listOpenIssueNumbers(options.repo, { limit: options.limit ?? 1000 });
450
+ const limit = options.limit ?? 1000;
451
+ const openNumbers = options.openNumbers ??
452
+ (options.listOpenFn ?? ((repo, listLimit) => listOpenIssueNumbers(repo, { limit: listLimit })))(options.repo, limit);
399
453
  return runStateRefresh({
400
454
  repo: options.repo,
401
455
  openNumbers,
@@ -410,4 +464,171 @@ export function cacheRefreshClosed(options) {
410
464
  },
411
465
  });
412
466
  }
467
+ export const DEFAULT_SELF_HEAL_TTL_SECONDS = 3600;
468
+ export const SELF_HEAL_STATE_FILENAME = "self-heal-state.json";
469
+ export const DEFAULT_MAX_CONTENT_DRIFT_CHECKS = 25;
470
+ function issueContentFingerprint(raw) {
471
+ const labels = (raw.labels ?? [])
472
+ .map((label) => String(label.name ?? ""))
473
+ .filter(Boolean)
474
+ .sort();
475
+ return JSON.stringify({
476
+ body: raw.body ?? "",
477
+ labels,
478
+ state: typeof raw.state === "string" ? raw.state.toLowerCase() : raw.state,
479
+ title: raw.title ?? "",
480
+ });
481
+ }
482
+ function scanCacheForSingleRepo(cacheRoot, source) {
483
+ const base = join(cacheRoot, source);
484
+ if (!existsSync(base))
485
+ return null;
486
+ const pairs = [];
487
+ for (const ownerEntry of readdirSync(base, { withFileTypes: true })) {
488
+ if (!ownerEntry.isDirectory())
489
+ continue;
490
+ const ownerDir = join(base, ownerEntry.name);
491
+ try {
492
+ for (const repoEntry of readdirSync(ownerDir, { withFileTypes: true })) {
493
+ if (!repoEntry.isDirectory())
494
+ continue;
495
+ pairs.push([ownerEntry.name, repoEntry.name]);
496
+ }
497
+ }
498
+ catch {
499
+ /* skip */
500
+ }
501
+ }
502
+ if (pairs.length === 1 && pairs[0]) {
503
+ return `${pairs[0][0]}/${pairs[0][1]}`;
504
+ }
505
+ return null;
506
+ }
507
+ /** Diff cached-open issue numbers vs the live open set and TTL-fresh content drift. */
508
+ export function probeCacheDrift(options) {
509
+ const source = options.source ?? "github-issue";
510
+ const cacheRoot = options.cacheRoot ?? ".deft-cache";
511
+ const limit = options.limit ?? 1000;
512
+ const includeContentDrift = options.includeContentDrift !== false;
513
+ const maxContentDriftChecks = options.maxContentDriftChecks ?? DEFAULT_MAX_CONTENT_DRIFT_CHECKS;
514
+ const listOpen = options.listOpenFn ??
515
+ ((repo, listLimit) => listOpenIssueNumbers(repo, { limit: listLimit }));
516
+ const fetchSingle = options.fetchSingleFn ?? singleIssueFetcherImpl;
517
+ const isFreshFn = options.isFreshFn ?? ((metaPath) => isFresh(metaPath));
518
+ const cachedOpen = scanCachedOpenEntries(options.repo, source, cacheRoot);
519
+ const liveOpen = listOpen(options.repo, limit);
520
+ const stateDriftNumbers = [];
521
+ const contentDriftNumbers = [];
522
+ let contentChecks = 0;
523
+ for (const [number, cachedRaw] of cachedOpen) {
524
+ if (!liveOpen.has(number)) {
525
+ stateDriftNumbers.push(number);
526
+ continue;
527
+ }
528
+ if (!includeContentDrift || contentChecks >= maxContentDriftChecks)
529
+ continue;
530
+ const parts = options.repo.split("/", 2);
531
+ const owner = parts[0] ?? "";
532
+ const name = parts[1] ?? "";
533
+ const metaPath = join(cacheRoot, source, owner, name, String(number), "meta.json");
534
+ if (!isFreshFn(metaPath))
535
+ continue;
536
+ contentChecks += 1;
537
+ try {
538
+ const live = normaliseRestIssue(fetchSingle(options.repo, number));
539
+ if (issueContentFingerprint(cachedRaw) !== issueContentFingerprint(live)) {
540
+ contentDriftNumbers.push(number);
541
+ }
542
+ }
543
+ catch {
544
+ /* probe best-effort */
545
+ }
546
+ }
547
+ stateDriftNumbers.sort((a, b) => a - b);
548
+ contentDriftNumbers.sort((a, b) => a - b);
549
+ return { stateDriftNumbers, contentDriftNumbers };
550
+ }
551
+ function readSelfHealState(cacheRoot) {
552
+ const path = join(cacheRoot, SELF_HEAL_STATE_FILENAME);
553
+ if (!existsSync(path))
554
+ return null;
555
+ try {
556
+ const raw = readFileSync(path, "utf8").trim();
557
+ if (raw.length === 0)
558
+ return null;
559
+ const parsed = JSON.parse(raw);
560
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
561
+ return null;
562
+ const stamp = parsed.last_reconcile_at;
563
+ if (typeof stamp !== "string")
564
+ return null;
565
+ const parsedDate = new Date(stamp);
566
+ return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
567
+ }
568
+ catch {
569
+ return null;
570
+ }
571
+ }
572
+ function writeSelfHealState(cacheRoot, when) {
573
+ const path = join(cacheRoot, SELF_HEAL_STATE_FILENAME);
574
+ writeFileSync(path, `${JSON.stringify({ last_reconcile_at: when.toISOString() })}\n`, "utf8");
575
+ }
576
+ /** TTL-bounded closed-reconcile for session ritual / triage:welcome self-healing (#1886). */
577
+ export function maybeSelfHealCache(projectRoot, options = {}) {
578
+ const source = options.source ?? "github-issue";
579
+ const cacheRoot = options.cacheRoot ?? join(projectRoot, ".deft-cache");
580
+ const now = options.nowFn?.() ?? new Date();
581
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_SELF_HEAL_TTL_SECONDS;
582
+ const repo = options.repo ?? scanCacheForSingleRepo(cacheRoot, source);
583
+ if (repo === null) {
584
+ return { skipped: true, skipReason: "repo-not-resolved", drift: null, refresh: null };
585
+ }
586
+ const lastHeal = readSelfHealState(cacheRoot);
587
+ const ttlExpired = lastHeal === null || now.getTime() - lastHeal.getTime() >= ttlSeconds * 1000;
588
+ if (!ttlExpired) {
589
+ return { skipped: true, skipReason: "ttl-fresh-no-drift", drift: null, refresh: null };
590
+ }
591
+ const limit = 1000;
592
+ const listOpen = options.listOpenFn ??
593
+ ((repo, listLimit) => listOpenIssueNumbers(repo, { limit: listLimit }));
594
+ let openNumbers;
595
+ try {
596
+ openNumbers = listOpen(repo, limit);
597
+ }
598
+ catch {
599
+ return { skipped: true, skipReason: "drift-probe-failed", drift: null, refresh: null };
600
+ }
601
+ let drift;
602
+ try {
603
+ drift = probeCacheDrift({
604
+ repo,
605
+ source,
606
+ cacheRoot,
607
+ limit,
608
+ listOpenFn: () => openNumbers,
609
+ fetchSingleFn: options.fetchSingleFn,
610
+ includeContentDrift: false,
611
+ });
612
+ }
613
+ catch {
614
+ return { skipped: true, skipReason: "drift-probe-failed", drift: null, refresh: null };
615
+ }
616
+ const refreshFn = options.refreshFn ??
617
+ ((opts) => cacheRefreshClosed({
618
+ source: opts.source,
619
+ repo: opts.repo,
620
+ cacheRoot: opts.cacheRoot,
621
+ openNumbers: opts.openNumbers,
622
+ }));
623
+ try {
624
+ const refresh = refreshFn({ source, repo, cacheRoot, openNumbers });
625
+ if (options.writeState !== false && refresh.refreshFailed === 0) {
626
+ writeSelfHealState(cacheRoot, now);
627
+ }
628
+ return { skipped: false, skipReason: null, drift, refresh };
629
+ }
630
+ catch {
631
+ return { skipped: true, skipReason: "refresh-failed", drift, refresh: null };
632
+ }
633
+ }
413
634
  //# sourceMappingURL=fetch.js.map
@@ -109,7 +109,7 @@ function cmdGet(args) {
109
109
  }
110
110
  catch (err) {
111
111
  if (err instanceof CacheNotFoundError) {
112
- process.stderr.write(`cache:get miss: ${JSON.stringify(err.message)}\n`);
112
+ process.stderr.write(`cache:get miss: ${err.message}\n`);
113
113
  return 1;
114
114
  }
115
115
  throw err;
@@ -150,7 +150,8 @@ function cmdFetchAll(args) {
150
150
  let limit = 1000;
151
151
  const labels = [];
152
152
  let author;
153
- let refreshClosed = false;
153
+ let skipRefreshClosed = false;
154
+ let force = false;
154
155
  for (let i = 0; i < args.length; i += 1) {
155
156
  const arg = args[i];
156
157
  if (arg === "--source") {
@@ -189,8 +190,11 @@ function cmdFetchAll(args) {
189
190
  author = args[i + 1];
190
191
  i += 1;
191
192
  }
192
- else if (arg === "--refresh-closed") {
193
- refreshClosed = true;
193
+ else if (arg === "--no-refresh-closed") {
194
+ skipRefreshClosed = true;
195
+ }
196
+ else if (arg === "--force" || arg === "--ignore-ttl") {
197
+ force = true;
194
198
  }
195
199
  }
196
200
  if (!source || !repo) {
@@ -207,10 +211,11 @@ function cmdFetchAll(args) {
207
211
  limit,
208
212
  labels: normaliseLabelFilter(labels),
209
213
  author: author ?? null,
214
+ force,
210
215
  });
211
216
  process.stdout.write(`${report.toJson()}\n`);
212
217
  let rc = report.issuesFailed === 0 ? 0 : 1;
213
- if (refreshClosed) {
218
+ if (!skipRefreshClosed) {
214
219
  const refresh = cacheRefreshClosed({
215
220
  source,
216
221
  repo,
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { pyRepr } from "../scm/py-format.js";
3
4
  import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
4
5
  import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
5
6
  import { appendAudit, atomicWriteText, fileSize, removeEntryDir, touchMtime } from "./io.js";
@@ -183,10 +184,11 @@ export function cacheGet(source, key, options = {}) {
183
184
  const cacheRoot = options.cacheRoot ?? ".deft-cache";
184
185
  const allowStale = options.allowStale ?? true;
185
186
  const edir = entryDir(source, key, cacheRoot);
186
- const metaPath = join(edir, "meta.json");
187
- if (!existsSync(metaPath)) {
188
- throw new CacheNotFoundError(`cache miss for source='${source}' key='${key}' (expected meta.json at ${metaPath})`);
187
+ const metaRelPath = `${source}/${key}/meta.json`;
188
+ if (!existsSync(join(edir, "meta.json"))) {
189
+ throw new CacheNotFoundError(`cache miss for source=${pyRepr(source)} key=${pyRepr(key)} (expected meta.json at ${metaRelPath})`);
189
190
  }
191
+ const metaPath = join(edir, "meta.json");
190
192
  let meta;
191
193
  try {
192
194
  meta = JSON.parse(readFileSync(metaPath, "utf8"));
@@ -199,7 +201,7 @@ export function cacheGet(source, key, options = {}) {
199
201
  const expires = parseIso(String(meta.expires_at));
200
202
  const isStale = clock.now() > expires;
201
203
  if (isStale && !allowStale) {
202
- throw new CacheNotFoundError(`cache entry stale for source='${source}' key='${key}'; expires_at=${meta.expires_at} (pass --allow-stale to override)`);
204
+ throw new CacheNotFoundError(`cache entry stale for source=${pyRepr(source)} key=${pyRepr(key)}; expires_at=${meta.expires_at} (pass --allow-stale to override)`);
203
205
  }
204
206
  meta.stale = isStale;
205
207
  touchMtime(metaPath);
@@ -8,8 +8,14 @@ export function checkCodebaseMapFresh(projectRoot, options) {
8
8
  const resolvedOutput = isAbsolute(options.outputPath)
9
9
  ? options.outputPath
10
10
  : join(projectRoot, options.outputPath);
11
+ // #1932: the generated MAP is an on-demand, gitignored artifact. An absent
12
+ // projection is OK (advisory) -- the gate must not force per-branch regeneration
13
+ // + commit, which guaranteed mechanical MAP.md collisions across concurrent
14
+ // branches. When a MAP IS present locally the freshness check below still flags
15
+ // it if stale; the durable plan.architecture.codeStructure stays gated by
16
+ // codebase:validate-structure.
11
17
  if (!existsSync(resolvedOutput)) {
12
- return [`generated codebase MAP is missing: ${resolvedOutput}`];
18
+ return [];
13
19
  }
14
20
  let current;
15
21
  try {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * copy-tree.ts — recursively copy a directory tree with file-mode preservation.
3
+ *
4
+ * Mirrors cmd/deft-install/setup.go `copyDir` / `copyFile` (#1477): intermediate
5
+ * directories are created mode 0o755; files keep their source permission bits
6
+ * (including the executable bit for hooks and the `run` launcher).
7
+ *
8
+ * Refs #1942 S1, #1477.
9
+ */
10
+ /**
11
+ * Recursively copy `src` into `dst`, preserving nested structure and file modes.
12
+ *
13
+ * The contents of `src` are placed under `dst` (equivalent to Go `copyDir`).
14
+ */
15
+ export declare function copyTree(src: string, dst: string): Promise<void>;
16
+ //# sourceMappingURL=copy-tree.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * copy-tree.ts — recursively copy a directory tree with file-mode preservation.
3
+ *
4
+ * Mirrors cmd/deft-install/setup.go `copyDir` / `copyFile` (#1477): intermediate
5
+ * directories are created mode 0o755; files keep their source permission bits
6
+ * (including the executable bit for hooks and the `run` launcher).
7
+ *
8
+ * Refs #1942 S1, #1477.
9
+ */
10
+ import { createReadStream, createWriteStream } from "node:fs";
11
+ import { chmod, mkdir, readdir, stat } from "node:fs/promises";
12
+ import { dirname, join } from "node:path";
13
+ import { pipeline } from "node:stream/promises";
14
+ const DEFAULT_FILE_MODE = 0o644;
15
+ const DEFAULT_DIR_MODE = 0o755;
16
+ async function copyFilePreserveMode(src, dst) {
17
+ let mode = DEFAULT_FILE_MODE;
18
+ try {
19
+ const info = await stat(src);
20
+ mode = info.mode & 0o777;
21
+ }
22
+ catch {
23
+ // Stat failure is non-fatal — fall back to 0o644 (mirrors Go copyFile).
24
+ }
25
+ await mkdir(dirname(dst), { recursive: true, mode: DEFAULT_DIR_MODE });
26
+ await pipeline(createReadStream(src), createWriteStream(dst, { mode }));
27
+ // createWriteStream mode applies on create; chmod ensures the final bits match
28
+ // the source even when the destination file already existed.
29
+ await chmod(dst, mode);
30
+ }
31
+ async function copyDirContents(src, dst) {
32
+ await mkdir(dst, { recursive: true, mode: DEFAULT_DIR_MODE });
33
+ const entries = await readdir(src, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const srcPath = join(src, entry.name);
36
+ const dstPath = join(dst, entry.name);
37
+ const srcStat = await stat(srcPath);
38
+ if (srcStat.isDirectory()) {
39
+ await copyDirContents(srcPath, dstPath);
40
+ }
41
+ else {
42
+ await copyFilePreserveMode(srcPath, dstPath);
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Recursively copy `src` into `dst`, preserving nested structure and file modes.
48
+ *
49
+ * The contents of `src` are placed under `dst` (equivalent to Go `copyDir`).
50
+ */
51
+ export async function copyTree(src, dst) {
52
+ const srcInfo = await stat(src);
53
+ if (!srcInfo.isDirectory()) {
54
+ throw new Error(`copyTree: source ${src} is not a directory`);
55
+ }
56
+ await copyDirContents(src, dst);
57
+ }
58
+ //# sourceMappingURL=copy-tree.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * resolve-content.ts — resolve the installed @deftai/directive-content package root.
3
+ *
4
+ * Used by the TS-native init/update deposit path (#1942 S1) to locate the npm
5
+ * content package before copying its tree into `.deft/core/`. Mirrors the
6
+ * package-resolution half of the Go installer's content fetch, but reads from
7
+ * the local node_modules install instead of a GitHub tarball.
8
+ *
9
+ * Refs #1942, #11, #1477.
10
+ */
11
+ export declare const CONTENT_PACKAGE_NAME = "@deftai/directive-content";
12
+ /** Thrown when {@link resolveInstalledContentRoot} cannot resolve the content package. */
13
+ export declare class ContentPackageNotFoundError extends Error {
14
+ readonly name = "ContentPackageNotFoundError";
15
+ }
16
+ type ResolveSpecifier = (specifier: string) => Promise<string>;
17
+ /**
18
+ * Walk upward from `resolvedEntry` until a `package.json` names
19
+ * {@link CONTENT_PACKAGE_NAME}; returns that directory.
20
+ */
21
+ export declare function contentPackageRootFromResolvedEntry(resolvedEntry: string): string;
22
+ /**
23
+ * Resolve the installed {@link CONTENT_PACKAGE_NAME} package root via Node module
24
+ * resolution (`import.meta.resolve`).
25
+ */
26
+ export declare function resolveInstalledContentRoot(resolveSpecifier?: ResolveSpecifier): Promise<string>;
27
+ export {};
28
+ //# sourceMappingURL=resolve-content.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * resolve-content.ts — resolve the installed @deftai/directive-content package root.
3
+ *
4
+ * Used by the TS-native init/update deposit path (#1942 S1) to locate the npm
5
+ * content package before copying its tree into `.deft/core/`. Mirrors the
6
+ * package-resolution half of the Go installer's content fetch, but reads from
7
+ * the local node_modules install instead of a GitHub tarball.
8
+ *
9
+ * Refs #1942, #11, #1477.
10
+ */
11
+ import { readFileSync, statSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ export const CONTENT_PACKAGE_NAME = "@deftai/directive-content";
15
+ /** Thrown when {@link resolveInstalledContentRoot} cannot resolve the content package. */
16
+ export class ContentPackageNotFoundError extends Error {
17
+ name = "ContentPackageNotFoundError";
18
+ }
19
+ async function defaultResolveSpecifier(specifier) {
20
+ return fileURLToPath(await import.meta.resolve(specifier));
21
+ }
22
+ /**
23
+ * Walk upward from `resolvedEntry` until a `package.json` names
24
+ * {@link CONTENT_PACKAGE_NAME}; returns that directory.
25
+ */
26
+ export function contentPackageRootFromResolvedEntry(resolvedEntry) {
27
+ let dir = statSync(resolvedEntry).isDirectory() ? resolvedEntry : dirname(resolvedEntry);
28
+ for (;;) {
29
+ const pkgJsonPath = join(dir, "package.json");
30
+ try {
31
+ if (statSync(pkgJsonPath).isFile()) {
32
+ const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
33
+ if (typeof parsed === "object" &&
34
+ parsed !== null &&
35
+ parsed.name === CONTENT_PACKAGE_NAME) {
36
+ return dir;
37
+ }
38
+ }
39
+ }
40
+ catch {
41
+ // Missing or unreadable package.json — keep walking.
42
+ }
43
+ const parent = dirname(dir);
44
+ if (parent === dir) {
45
+ break;
46
+ }
47
+ dir = parent;
48
+ }
49
+ throw new ContentPackageNotFoundError(`Resolved ${CONTENT_PACKAGE_NAME} entry at ${resolvedEntry}, but no matching package root was found. ` +
50
+ "Reinstall with `pnpm add @deftai/directive-content` or `npm i @deftai/directive-content`.");
51
+ }
52
+ /**
53
+ * Resolve the installed {@link CONTENT_PACKAGE_NAME} package root via Node module
54
+ * resolution (`import.meta.resolve`).
55
+ */
56
+ export async function resolveInstalledContentRoot(resolveSpecifier = defaultResolveSpecifier) {
57
+ let resolvedEntry;
58
+ try {
59
+ resolvedEntry = await resolveSpecifier(CONTENT_PACKAGE_NAME);
60
+ }
61
+ catch (cause) {
62
+ const detail = cause instanceof Error ? cause.message : String(cause);
63
+ throw new ContentPackageNotFoundError(`${CONTENT_PACKAGE_NAME} is not installed or could not be resolved. ` +
64
+ "Install it with `pnpm add @deftai/directive-content` or `npm i @deftai/directive-content` " +
65
+ "so init/update can deposit framework content locally. " +
66
+ `Resolution error: ${detail}`);
67
+ }
68
+ return contentPackageRootFromResolvedEntry(resolvedEntry);
69
+ }
70
+ //# sourceMappingURL=resolve-content.js.map
@@ -8,6 +8,12 @@ export declare function checkQuickStartResolves(projectRoot: string, installRoot
8
8
  export declare function checkSkillPathsResolve(projectRoot: string, agentsMdText: string, seams?: CheckSeams): CheckResult;
9
9
  export declare function checkManifestAgreement(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
10
10
  export declare function checkInstallPathConsistency(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
11
+ /**
12
+ * #1912: signpost a legacy on-disk layout. Carries the STABLE UPGRADING.md URL
13
+ * only -- no baked Go-installer version or literal upgrade command. Returns
14
+ * `skip` for the canonical / greenfield layout (nothing to signpost).
15
+ */
16
+ export declare function checkLegacyLayout(projectRoot: string, seams?: CheckSeams): CheckResult;
11
17
  export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
12
18
  export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
13
19
  isDir?: (p: string) => boolean;