@deftai/directive-core 0.55.2 → 0.56.1
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.
- package/dist/cache/errors.d.ts +4 -1
- package/dist/cache/errors.js +8 -2
- package/dist/cache/fetch.d.ts +50 -0
- package/dist/cache/fetch.js +224 -3
- package/dist/cache/main.js +10 -5
- package/dist/cache/operations.js +6 -4
- package/dist/deposit/copy-tree.d.ts +16 -0
- package/dist/deposit/copy-tree.js +58 -0
- package/dist/deposit/resolve-content.d.ts +28 -0
- package/dist/deposit/resolve-content.js +70 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +37 -0
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/gitignore.d.ts +51 -0
- package/dist/init-deposit/gitignore.js +200 -0
- package/dist/init-deposit/index.d.ts +6 -0
- package/dist/init-deposit/index.js +6 -0
- package/dist/init-deposit/init-deposit.d.ts +44 -0
- package/dist/init-deposit/init-deposit.js +197 -0
- package/dist/init-deposit/legacy-detect.d.ts +61 -0
- package/dist/init-deposit/legacy-detect.js +223 -0
- package/dist/init-deposit/migrate.d.ts +88 -0
- package/dist/init-deposit/migrate.js +196 -0
- package/dist/init-deposit/refresh.d.ts +50 -0
- package/dist/init-deposit/refresh.js +292 -0
- package/dist/init-deposit/scaffold.d.ts +41 -0
- package/dist/init-deposit/scaffold.js +753 -0
- package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
- package/dist/legacy-bridge/bridge-drift.js +171 -0
- package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
- package/dist/legacy-bridge/freeze-gate.js +168 -0
- package/dist/legacy-bridge/index.d.ts +4 -0
- package/dist/legacy-bridge/index.js +4 -0
- package/dist/legacy-bridge/sot.d.ts +45 -0
- package/dist/legacy-bridge/sot.js +49 -0
- package/dist/preflight-cache/evaluate.d.ts +11 -0
- package/dist/preflight-cache/evaluate.js +63 -13
- package/dist/release-e2e/flags.js +5 -1
- package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
- package/dist/release-e2e/legacy-bridge-leg.js +478 -0
- package/dist/release-e2e/main.d.ts +2 -1
- package/dist/release-e2e/main.js +17 -1
- package/dist/release-e2e/types.d.ts +9 -0
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/project-render.js +4 -3
- package/dist/render/roadmap-render.d.ts +4 -2
- package/dist/render/roadmap-render.js +14 -7
- package/dist/triage/actions/candidates-log.d.ts +12 -1
- package/dist/triage/actions/candidates-log.js +44 -7
- package/dist/triage/actions/index.d.ts +25 -1
- package/dist/triage/actions/index.js +94 -3
- package/dist/triage/summary/index.d.ts +1 -3
- package/dist/triage/summary/index.js +6 -57
- package/dist/triage/summary/reconcilable.js +4 -33
- package/dist/triage/welcome/default-mode.d.ts +1 -0
- package/dist/triage/welcome/default-mode.js +6 -0
- package/dist/vbrief-validate/project-definition.js +4 -69
- package/dist/vbrief-validate/registry-status.d.ts +20 -0
- package/dist/vbrief-validate/registry-status.js +85 -0
- package/package.json +11 -2
package/dist/cache/errors.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/cache/errors.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
14
|
+
innerMessage;
|
|
15
|
+
constructor(inner) {
|
|
16
|
+
super(keyErrorMessage(inner));
|
|
17
|
+
this.innerMessage = inner;
|
|
12
18
|
this.name = "CacheNotFoundError";
|
|
13
19
|
}
|
|
14
20
|
}
|
package/dist/cache/fetch.d.ts
CHANGED
|
@@ -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
|
package/dist/cache/fetch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
package/dist/cache/main.js
CHANGED
|
@@ -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: ${
|
|
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
|
|
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
|
-
|
|
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 (
|
|
218
|
+
if (!skipRefreshClosed) {
|
|
214
219
|
const refresh = cacheRefreshClosed({
|
|
215
220
|
source,
|
|
216
221
|
repo,
|
package/dist/cache/operations.js
CHANGED
|
@@ -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
|
|
187
|
-
if (!existsSync(
|
|
188
|
-
throw new CacheNotFoundError(`cache miss for source
|
|
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
|
|
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);
|
|
@@ -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
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -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;
|