@deftai/directive-core 0.93.0 → 0.95.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.
- package/dist/authz/classify.js +485 -0
- package/dist/cache/fetch.d.ts +29 -0
- package/dist/cache/fetch.js +131 -4
- package/dist/cache/operations.d.ts +8 -0
- package/dist/cache/operations.js +67 -4
- package/dist/check/cached-orchestrator.js +11 -0
- package/dist/check/consumer-gate-integrity.d.ts +68 -0
- package/dist/check/consumer-gate-integrity.js +265 -0
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/orchestrator.js +10 -0
- package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
- package/dist/content-contracts/skills/greptile-detector.js +9 -2
- package/dist/doctor/main.js +98 -9
- package/dist/doctor/types.d.ts +2 -2
- package/dist/finish-loop/pr-finish-loop.js +1 -0
- package/dist/freshness/bind.d.ts +67 -0
- package/dist/freshness/bind.js +201 -0
- package/dist/freshness/cli.d.ts +30 -0
- package/dist/freshness/cli.js +180 -0
- package/dist/freshness/compare.d.ts +14 -0
- package/dist/freshness/compare.js +134 -0
- package/dist/freshness/generation.d.ts +44 -0
- package/dist/freshness/generation.js +172 -0
- package/dist/freshness/index.d.ts +13 -0
- package/dist/freshness/index.js +13 -0
- package/dist/freshness/report.d.ts +43 -0
- package/dist/freshness/report.js +139 -0
- package/dist/freshness/types.d.ts +70 -0
- package/dist/freshness/types.js +30 -0
- package/dist/handoff-evidence/index.d.ts +8 -0
- package/dist/handoff-evidence/index.js +7 -0
- package/dist/handoff-evidence/validate.d.ts +105 -0
- package/dist/handoff-evidence/validate.js +423 -0
- package/dist/hooks/dispatcher.d.ts +27 -2
- package/dist/hooks/dispatcher.js +134 -14
- package/dist/hooks/readonly.d.ts +14 -0
- package/dist/hooks/readonly.js +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/init-deposit.d.ts +7 -1
- package/dist/init-deposit/init-deposit.js +24 -4
- package/dist/init-deposit/refresh.d.ts +9 -1
- package/dist/init-deposit/refresh.js +48 -5
- package/dist/intake/issue-ingest.d.ts +20 -0
- package/dist/intake/issue-ingest.js +58 -0
- package/dist/policy/index.d.ts +4 -0
- package/dist/policy/index.js +50 -1
- package/dist/policy/min-greptile-confidence.d.ts +57 -0
- package/dist/policy/min-greptile-confidence.js +123 -0
- package/dist/pr-merge-readiness/compute.d.ts +11 -1
- package/dist/pr-merge-readiness/compute.js +17 -4
- package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
- package/dist/pr-merge-readiness/evaluate.js +7 -4
- package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
- package/dist/pr-merge-readiness/mergeability.js +14 -2
- package/dist/pr-monitor/main.d.ts +2 -0
- package/dist/pr-monitor/main.js +31 -1
- package/dist/pr-monitor/monitor.js +5 -1
- package/dist/pr-monitor/readiness.js +3 -1
- package/dist/pr-monitor/types.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -1
- package/dist/pr-wait-mergeable/main.d.ts +2 -0
- package/dist/pr-wait-mergeable/main.js +16 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +3 -0
- package/dist/pr-watch/constants.d.ts +4 -1
- package/dist/pr-watch/constants.js +5 -2
- package/dist/pr-watch/main.js +1 -0
- package/dist/pr-watch/probe.d.ts +1 -1
- package/dist/pr-watch/probe.js +4 -1
- package/dist/pr-watch/types.d.ts +7 -1
- package/dist/pr-watch/watch.js +3 -2
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/review-monitor/index.d.ts +1 -0
- package/dist/review-monitor/index.js +1 -0
- package/dist/review-monitor/l4-owner.d.ts +46 -0
- package/dist/review-monitor/l4-owner.js +262 -0
- package/dist/session/ritual-entrypoint.d.ts +6 -2
- package/dist/session/ritual-entrypoint.js +15 -2
- package/dist/session/session-ready.js +31 -14
- package/dist/session/session-start.d.ts +2 -1
- package/dist/session/session-start.js +66 -3
- package/dist/session/verify-session-ritual.d.ts +4 -2
- package/dist/session/verify-session-ritual.js +11 -11
- package/dist/slash/product-set.js +4 -1
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/tool-events/classify.js +1 -0
- package/dist/triage/classify/index.d.ts +9 -0
- package/dist/triage/classify/index.js +34 -2
- package/dist/triage/classify/label-mirror.d.ts +143 -0
- package/dist/triage/classify/label-mirror.js +684 -0
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +27 -7
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- package/dist/triage/welcome/writers.js +16 -7
- package/dist/umbrella-current-shape/index.d.ts +45 -0
- package/dist/umbrella-current-shape/index.js +162 -8
- package/dist/vbrief-reconcile/index.d.ts +2 -2
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
- package/dist/vbrief-reconcile/umbrellas.js +231 -19
- package/dist/vbrief-validate/project-definition.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
- package/dist/verify-env/agent-hook-readiness.js +150 -0
- package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
- package/dist/verify-env/agent-hooks-live-probe.js +120 -107
- package/dist/verify-env/agent-hooks.js +11 -3
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/package.json +7 -3
package/dist/cache/fetch.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
|
|
4
|
-
import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
|
|
4
|
+
import { GhRestError, InvalidRepoError, restIssueView, runGhApi, splitRepo, } from "../scm/gh-rest.js";
|
|
5
|
+
import { CURRENT_SHAPE_SIDECAR, isUmbrellaLikeIssue, parseCommentsFromGhStdout, RAW_ISSUE_COMMENTS_KEY, } from "../umbrella-current-shape/index.js";
|
|
5
6
|
import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
|
|
6
7
|
import { CacheError, CacheFetchError } from "./errors.js";
|
|
7
8
|
import { atomicWriteText } from "./io.js";
|
|
@@ -54,20 +55,130 @@ export const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
|
|
|
54
55
|
export const PROGRESS_EVERY_N = 50;
|
|
55
56
|
const RATE_LIMIT_RE = /(?:HTTP\s*429|API rate limit exceeded|rate limit exceeded)/i;
|
|
56
57
|
const RETRY_AFTER_RE = /Retry-After:\s*(\d+)/i;
|
|
58
|
+
/**
|
|
59
|
+
* Default REST fetch for an issue's comment thread (#1870).
|
|
60
|
+
* Returns raw REST comment objects (with id/body/html_url/author_association).
|
|
61
|
+
*/
|
|
62
|
+
export function restIssueComments(repo, issueNumber, options = {}) {
|
|
63
|
+
const [owner, name] = splitRepo(repo);
|
|
64
|
+
const endpoint = `repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100`;
|
|
65
|
+
const runner = options.runGhApiFn ?? runGhApi;
|
|
66
|
+
const result = runner(["--paginate", endpoint]);
|
|
67
|
+
if (result.returncode !== 0) {
|
|
68
|
+
throw new GhRestError({
|
|
69
|
+
stderr: result.stderr.trim(),
|
|
70
|
+
exitCode: result.returncode,
|
|
71
|
+
endpoint,
|
|
72
|
+
payload: null,
|
|
73
|
+
hint: "verify repo/issue number and gh auth; comments feed the #1152 current-shape surface",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
// Reuse the paginated-JSON parser so multi-page comment threads stay intact.
|
|
78
|
+
const mapped = parseCommentsFromGhStdout(result.stdout);
|
|
79
|
+
return mapped.map((c) => ({
|
|
80
|
+
id: c.id,
|
|
81
|
+
body: c.body,
|
|
82
|
+
html_url: c.htmlUrl,
|
|
83
|
+
updated_at: c.updatedAt,
|
|
84
|
+
author_association: c.authorAssociation,
|
|
85
|
+
user: { login: c.authorLogin },
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
catch (exc) {
|
|
89
|
+
throw new GhRestError({
|
|
90
|
+
stderr: `non-JSON comments response: ${exc instanceof Error ? exc.message : String(exc)}`,
|
|
91
|
+
exitCode: 0,
|
|
92
|
+
endpoint,
|
|
93
|
+
payload: null,
|
|
94
|
+
hint: "REST comments endpoint returned non-JSON; check gh / ghx version",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
57
98
|
let paginatedListerImpl = restIssueListPaginated;
|
|
58
99
|
let sleepImpl = () => { };
|
|
59
100
|
let progressWriterImpl = (line) => process.stderr.write(line);
|
|
60
101
|
let progressFlusherImpl = () => { };
|
|
61
102
|
let singleIssueFetcherImpl = restIssueView;
|
|
103
|
+
let issueCommentsFetcherImpl = restIssueComments;
|
|
62
104
|
export function setSingleIssueFetcher(fn) {
|
|
63
105
|
singleIssueFetcherImpl = fn;
|
|
64
106
|
}
|
|
107
|
+
export function setIssueCommentsFetcher(fn) {
|
|
108
|
+
issueCommentsFetcherImpl = fn;
|
|
109
|
+
}
|
|
65
110
|
export function setPaginatedLister(fn) {
|
|
66
111
|
paginatedListerImpl = fn;
|
|
67
112
|
}
|
|
68
113
|
export function setSleepFn(fn) {
|
|
69
114
|
sleepImpl = fn;
|
|
70
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Attach the issue comment thread onto a raw issue payload when the issue is
|
|
118
|
+
* umbrella/tracker-like (#1870). Non-umbrellas skip the extra REST call.
|
|
119
|
+
* Failures leave the payload unchanged so body-only cache still lands.
|
|
120
|
+
*
|
|
121
|
+
* When `force` is true, re-fetches even if a comments array is already present
|
|
122
|
+
* (closed-state refresh and shape backfill paths).
|
|
123
|
+
*/
|
|
124
|
+
export function enrichRawWithCommentsIfUmbrella(repo, raw, options = {}) {
|
|
125
|
+
if (!isUmbrellaLikeIssue(raw)) {
|
|
126
|
+
return raw;
|
|
127
|
+
}
|
|
128
|
+
if (options.force !== true && Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY])) {
|
|
129
|
+
return raw;
|
|
130
|
+
}
|
|
131
|
+
const number = raw.number;
|
|
132
|
+
if (typeof number !== "number" || number <= 0) {
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
const fetcher = options.fetchComments ?? issueCommentsFetcherImpl;
|
|
136
|
+
try {
|
|
137
|
+
const comments = fetcher(repo, number);
|
|
138
|
+
return { ...raw, [RAW_ISSUE_COMMENTS_KEY]: [...comments] };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Soft-fail: cache body still useful; agents can live-fetch via umbrella:current-shape.
|
|
142
|
+
// Preserve a prior comments array when force-refresh fails so we do not
|
|
143
|
+
// drop a previously cached current-shape surface (#1870 Greptile P1).
|
|
144
|
+
if (Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY])) {
|
|
145
|
+
return raw;
|
|
146
|
+
}
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* True when an on-disk cache entry is umbrella-like but was written before
|
|
152
|
+
* #1870 comment enrichment (no `comments` key on raw.json). Used to override
|
|
153
|
+
* the TTL-fresh skip so body-only entries get one backfill pass.
|
|
154
|
+
*/
|
|
155
|
+
export function entryNeedsCurrentShapeBackfill(edir) {
|
|
156
|
+
const rawPath = join(edir, "raw.json");
|
|
157
|
+
if (!existsSync(rawPath)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const raw = JSON.parse(readFileSync(rawPath, "utf8"));
|
|
162
|
+
if (!isUmbrellaLikeIssue(raw)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// Missing comments key → never enriched. Empty array is "attempted".
|
|
166
|
+
if (!Object.hasOwn(raw, RAW_ISSUE_COMMENTS_KEY)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
// Has comments but no sidecar while a shape should exist: rare mid-write
|
|
170
|
+
// race; treat missing sidecar + non-empty comments as needing rewrite.
|
|
171
|
+
if (Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY]) &&
|
|
172
|
+
raw[RAW_ISSUE_COMMENTS_KEY].length > 0 &&
|
|
173
|
+
!existsSync(join(edir, CURRENT_SHAPE_SIDECAR))) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
71
182
|
export function setProgressWriter(fn, flusher) {
|
|
72
183
|
progressWriterImpl = fn;
|
|
73
184
|
progressFlusherImpl = flusher ?? (() => { });
|
|
@@ -311,7 +422,7 @@ export function runFetchAll(options) {
|
|
|
311
422
|
}
|
|
312
423
|
for (let i = 0; i < issues.length; i += 1) {
|
|
313
424
|
const processed = i + 1;
|
|
314
|
-
|
|
425
|
+
let raw = normaliseRestIssue(issues[i] ?? {});
|
|
315
426
|
const number = raw.number;
|
|
316
427
|
if (typeof number !== "number" || number <= 0) {
|
|
317
428
|
report.issuesFailed += 1;
|
|
@@ -324,12 +435,24 @@ export function runFetchAll(options) {
|
|
|
324
435
|
const key = `${options.repo}/${number}`;
|
|
325
436
|
const edir = entryDir(source, key, cacheRoot);
|
|
326
437
|
const metaPath = join(edir, "meta.json");
|
|
327
|
-
|
|
438
|
+
// #1870 Greptile P1: TTL-fresh must not skip body-only umbrella entries
|
|
439
|
+
// forever — comment enrichment is not part of the content-drift fingerprint.
|
|
440
|
+
const needsShapeBackfill = entryNeedsCurrentShapeBackfill(edir);
|
|
441
|
+
const skipFresh = !options.force &&
|
|
442
|
+
isFreshFn(metaPath) &&
|
|
443
|
+
!contentDriftNumbers.has(number) &&
|
|
444
|
+
!needsShapeBackfill;
|
|
328
445
|
if (skipFresh) {
|
|
329
446
|
report.alreadyFresh += 1;
|
|
330
447
|
}
|
|
331
448
|
else {
|
|
332
449
|
try {
|
|
450
|
+
// #1870: umbrella/tracker issues get comment threads so content.md
|
|
451
|
+
// carries the canonical ## Current shape surface, not body alone.
|
|
452
|
+
raw = enrichRawWithCommentsIfUmbrella(options.repo, raw, {
|
|
453
|
+
fetchComments: options.fetchComments,
|
|
454
|
+
force: needsShapeBackfill,
|
|
455
|
+
});
|
|
333
456
|
doPutFn(key, raw);
|
|
334
457
|
report.issuesWritten += 1;
|
|
335
458
|
}
|
|
@@ -388,6 +511,7 @@ export function cacheFetchAll(options) {
|
|
|
388
511
|
force: options.force,
|
|
389
512
|
contentDriftNumbers: options.contentDriftNumbers,
|
|
390
513
|
probeDriftFn: options.probeDriftFn,
|
|
514
|
+
fetchComments: options.fetchComments,
|
|
391
515
|
});
|
|
392
516
|
}
|
|
393
517
|
export class StateRefreshReportImpl {
|
|
@@ -509,7 +633,10 @@ export function cacheRefreshClosed(options) {
|
|
|
509
633
|
delayMs: options.delayMs ?? DEFAULT_DELAY_MS,
|
|
510
634
|
fetchSingle: singleIssueFetcherImpl,
|
|
511
635
|
doPut: (key, raw) => {
|
|
512
|
-
|
|
636
|
+
// #1870 Greptile P1: closed-state rewrite still enriches umbrella comments
|
|
637
|
+
// so current-shape.json is not deleted by a body-only single-issue put.
|
|
638
|
+
const enriched = enrichRawWithCommentsIfUmbrella(options.repo, raw, { force: true });
|
|
639
|
+
cachePut(options.source, key, enriched, {
|
|
513
640
|
ttlSeconds: options.ttlSeconds,
|
|
514
641
|
cacheRoot,
|
|
515
642
|
});
|
|
@@ -4,6 +4,14 @@ import { SCANNER_VERSION } from "./scanner.js";
|
|
|
4
4
|
import { type Clock } from "./time.js";
|
|
5
5
|
import type { CacheGetOptions, CachePutOptions, GetResult, PutResult } from "./types.js";
|
|
6
6
|
export { ProjectionContainmentError as CacheContainmentError };
|
|
7
|
+
/**
|
|
8
|
+
* Render agent-facing content.md for a cache entry.
|
|
9
|
+
*
|
|
10
|
+
* For github-issue sources, appends the canonical `## Current shape` comment
|
|
11
|
+
* when the raw payload carries a comment thread with a maintainer-authored
|
|
12
|
+
* shape (#1870 / #1152). Body alone is never sufficient for umbrella planning.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderContent(source: string, raw: Record<string, unknown>): string;
|
|
7
15
|
/** Write a cache entry (mirrors `cache.cache_put`). */
|
|
8
16
|
export declare function cachePut(source: string, key: string, raw: Record<string, unknown>, options?: CachePutOptions): PutResult;
|
|
9
17
|
/** Read a cache entry (mirrors `cache.cache_get`). */
|
package/dist/cache/operations.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
4
|
import { pyRepr } from "../scm/py-format.js";
|
|
5
|
+
import { appendCurrentShapeSection, buildCurrentShapeSidecar, CURRENT_SHAPE_SIDECAR, } from "../umbrella-current-shape/index.js";
|
|
5
6
|
import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
|
|
6
7
|
import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
|
|
7
8
|
import { appendAudit, atomicWriteText, fileSize, removeEntryDir, touchMtime } from "./io.js";
|
|
@@ -20,7 +21,14 @@ function assertWritableCachePath(cacheRoot, ...segments) {
|
|
|
20
21
|
assertProjectionContained(projectDir, target);
|
|
21
22
|
return target;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Render agent-facing content.md for a cache entry.
|
|
26
|
+
*
|
|
27
|
+
* For github-issue sources, appends the canonical `## Current shape` comment
|
|
28
|
+
* when the raw payload carries a comment thread with a maintainer-authored
|
|
29
|
+
* shape (#1870 / #1152). Body alone is never sufficient for umbrella planning.
|
|
30
|
+
*/
|
|
31
|
+
export function renderContent(source, raw) {
|
|
24
32
|
if (source === "github-issue") {
|
|
25
33
|
const number = raw.number;
|
|
26
34
|
const title = raw.title ?? "";
|
|
@@ -28,7 +36,8 @@ function renderContent(source, raw) {
|
|
|
28
36
|
if (typeof number !== "number") {
|
|
29
37
|
throw new CacheError(`invalid github-issue raw payload: 'number' must be int (got ${typeof number})`);
|
|
30
38
|
}
|
|
31
|
-
|
|
39
|
+
const base = `# #${number}: ${title}\n\n${body}`;
|
|
40
|
+
return appendCurrentShapeSection(base, raw);
|
|
32
41
|
}
|
|
33
42
|
throw new CacheError(`unknown source '${source}': v1 supports ${JSON.stringify([...ALLOWED_SOURCES].sort())}`);
|
|
34
43
|
}
|
|
@@ -96,9 +105,20 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
96
105
|
const edir = entryDir(source, key, cacheRoot);
|
|
97
106
|
const rawText = pythonJsonDump(raw);
|
|
98
107
|
const rawSize = Buffer.byteLength(rawText, "utf8");
|
|
108
|
+
// #1870 Greptile P2: account for current-shape sidecar bytes in cap + meta
|
|
109
|
+
// so usage does not underreport by the cumulative sidecar size.
|
|
110
|
+
const plannedSidecar = buildCurrentShapeSidecar(raw);
|
|
111
|
+
const plannedSidecarText = plannedSidecar !== null
|
|
112
|
+
? `${pythonJsonDump({
|
|
113
|
+
...plannedSidecar,
|
|
114
|
+
body: plannedSidecar.body,
|
|
115
|
+
})}\n`
|
|
116
|
+
: "";
|
|
117
|
+
const plannedSidecarBytes = Buffer.byteLength(plannedSidecarText, "utf8");
|
|
118
|
+
const entryPayloadBytes = rawSize + plannedSidecarBytes;
|
|
99
119
|
const existingSize = existingEntrySize(edir);
|
|
100
120
|
const isNewEntry = existingSize === null;
|
|
101
|
-
const incomingDelta = isNewEntry ?
|
|
121
|
+
const incomingDelta = isNewEntry ? entryPayloadBytes : entryPayloadBytes - existingSize;
|
|
102
122
|
const incomingEntries = isNewEntry ? 1 : 0;
|
|
103
123
|
const enforceResult = enforceCaps(cacheRoot, {
|
|
104
124
|
caps: options.caps ?? null,
|
|
@@ -132,14 +152,48 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
132
152
|
// #3042: contain cache entry writes against project root (parent of cacheRoot).
|
|
133
153
|
const projectRoot = dirname(resolve(cacheRoot));
|
|
134
154
|
atomicWriteText(join(edir, "raw.json"), rawText, { projectRoot });
|
|
135
|
-
const
|
|
155
|
+
const rawFileSize = fileSize(join(edir, "raw.json"));
|
|
136
156
|
const rendered = renderContent(source, raw);
|
|
137
157
|
const scanResult = scan(rendered, utcIso(clock, fetched));
|
|
138
158
|
const contentPath = join(edir, "content.md");
|
|
159
|
+
const shapeSidecarPath = join(edir, CURRENT_SHAPE_SIDECAR);
|
|
139
160
|
let contentWritten = false;
|
|
161
|
+
let sidecarBytesWritten = 0;
|
|
140
162
|
if (scanResult.passed) {
|
|
141
163
|
atomicWriteText(contentPath, scanResult.transformed_content, { projectRoot });
|
|
142
164
|
contentWritten = true;
|
|
165
|
+
// #1870: persist the selected current-shape comment + permalink beside
|
|
166
|
+
// content.md so triage/gates can read it without re-parsing the markdown.
|
|
167
|
+
const sidecar = plannedSidecar;
|
|
168
|
+
if (sidecar !== null) {
|
|
169
|
+
// Quarantine-scan the sidecar body independently so a hard-fail on the
|
|
170
|
+
// full content.md transform cannot leave a credential-bearing sidecar.
|
|
171
|
+
const shapeScan = scan(sidecar.body, utcIso(clock, fetched));
|
|
172
|
+
if (shapeScan.passed) {
|
|
173
|
+
const sidecarText = `${pythonJsonDump({
|
|
174
|
+
...sidecar,
|
|
175
|
+
body: shapeScan.transformed_content,
|
|
176
|
+
})}\n`;
|
|
177
|
+
atomicWriteText(shapeSidecarPath, sidecarText, { projectRoot });
|
|
178
|
+
sidecarBytesWritten = Buffer.byteLength(sidecarText, "utf8");
|
|
179
|
+
}
|
|
180
|
+
else if (existsSync(shapeSidecarPath)) {
|
|
181
|
+
try {
|
|
182
|
+
unlinkSync(shapeSidecarPath);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
/* ignore */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (existsSync(shapeSidecarPath)) {
|
|
190
|
+
try {
|
|
191
|
+
unlinkSync(shapeSidecarPath);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* ignore */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
143
197
|
}
|
|
144
198
|
else if (existsSync(contentPath)) {
|
|
145
199
|
try {
|
|
@@ -148,7 +202,16 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
148
202
|
catch {
|
|
149
203
|
/* ignore */
|
|
150
204
|
}
|
|
205
|
+
if (existsSync(shapeSidecarPath)) {
|
|
206
|
+
try {
|
|
207
|
+
unlinkSync(shapeSidecarPath);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* ignore */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
151
213
|
}
|
|
214
|
+
const authoritativeSize = rawFileSize + sidecarBytesWritten;
|
|
152
215
|
const meta = buildMeta({
|
|
153
216
|
source,
|
|
154
217
|
key,
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { lintShippedRegistry, resolveTaskContract, runWithCache, } from "../cache/task-cache/index.js";
|
|
4
4
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
5
|
+
import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
|
|
5
6
|
import { resolveCheckTarget } from "./context.js";
|
|
6
7
|
import { checkGateId, checkGateSpawnArgs, gatesForCheckTarget } from "./gate-lists.js";
|
|
7
8
|
function captureSpawn(taskBin, args, opts) {
|
|
@@ -29,6 +30,16 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
|
|
|
29
30
|
const cwd = target === "check:framework-source" ? resolvedFramework : resolvedProject;
|
|
30
31
|
const gates = gatesForCheckTarget(target);
|
|
31
32
|
const codeVersion = readCorePackageVersion();
|
|
33
|
+
// #3070: fail loud with deposit-repair guidance when consumer check-graph
|
|
34
|
+
// includes (e.g. tasks/verify.yml for verify:orphan-active) are missing,
|
|
35
|
+
// instead of opaque go-task "Task does not exist" exit 200/201.
|
|
36
|
+
if (target === "check:consumer") {
|
|
37
|
+
const integrity = evaluateConsumerGateIntegrity(resolvedFramework);
|
|
38
|
+
if (!integrity.ok) {
|
|
39
|
+
process.stderr.write(formatConsumerGateIntegrityFailure(integrity));
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
const registryLint = lintShippedRegistry();
|
|
33
44
|
if (!registryLint.ok) {
|
|
34
45
|
for (const finding of registryLint.findings.filter((f) => f.kind === "under-declared-input")) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer check-graph integrity (#3070).
|
|
3
|
+
*
|
|
4
|
+
* CONSUMER_CHECK_GATES lists Taskfile tasks that must resolve in a vendored
|
|
5
|
+
* deposit. When optional Taskfile includes silently omit `tasks/verify.yml`
|
|
6
|
+
* (etc.), go-task fails with opaque exit 200/201 ("Task does not exist").
|
|
7
|
+
*
|
|
8
|
+
* This module:
|
|
9
|
+
* 1. Maps each gate to its include namespace / root Taskfile surface
|
|
10
|
+
* 2. Proves shipped files define those tasks (static + runtime)
|
|
11
|
+
* 3. Emits a deposit-repair recovery message instead of opaque go-task errors
|
|
12
|
+
*/
|
|
13
|
+
import { type CheckGateSpec } from "./gate-lists.js";
|
|
14
|
+
/** Include namespaces required by the consumer check graph (Taskfile includes). */
|
|
15
|
+
export declare const CHECK_GRAPH_REQUIRED_NAMESPACES: readonly ["verify", "toolchain", "vbrief"];
|
|
16
|
+
export type CheckGraphNamespace = (typeof CHECK_GRAPH_REQUIRED_NAMESPACES)[number];
|
|
17
|
+
export declare const CONSUMER_GATE_INTEGRITY_RECOVERY: string;
|
|
18
|
+
export interface ConsumerGateIntegrityFinding {
|
|
19
|
+
readonly gateId: string;
|
|
20
|
+
readonly kind: "missing-include-file" | "missing-task-definition" | "missing-root-taskfile";
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
readonly expectedPath?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ConsumerGateIntegrityResult {
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
readonly findings: readonly ConsumerGateIntegrityFinding[];
|
|
27
|
+
readonly recovery: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ConsumerGateIntegritySeams {
|
|
30
|
+
readonly exists?: (path: string) => boolean;
|
|
31
|
+
readonly readText?: (path: string) => string | null;
|
|
32
|
+
/** Override gate list under test (defaults to CONSUMER_CHECK_GATES). */
|
|
33
|
+
readonly gates?: readonly CheckGateSpec[];
|
|
34
|
+
}
|
|
35
|
+
/** Namespace for a namespaced task (`verify:orphan-active` → `verify`); null for root tasks. */
|
|
36
|
+
export declare function gateNamespace(gateId: string): string | null;
|
|
37
|
+
/** Local task name inside an include (`verify:orphan-active` → `orphan-active`). */
|
|
38
|
+
export declare function gateLocalName(gateId: string): string;
|
|
39
|
+
/** Relative path of the include Taskfile for a namespace. */
|
|
40
|
+
export declare function includeTaskfileRel(namespace: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Namespaces that CONSUMER_CHECK_GATES require via `ns:task` form.
|
|
43
|
+
* Root-only gates (doctor, verify-strategy-output) do not add a namespace.
|
|
44
|
+
*/
|
|
45
|
+
export declare function requiredNamespacesForGates(gates?: readonly CheckGateSpec[]): readonly string[];
|
|
46
|
+
/** True when `localName` is a top-level task key under a Taskfile `tasks:` section. */
|
|
47
|
+
export declare function taskDefinedInTaskfileYaml(text: string, localName: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Parse root Taskfile `includes:` entries: map namespace → { taskfile, optional }.
|
|
50
|
+
* Minimal line parser (avoids a YAML dependency); sufficient for go-task include shape.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseTaskfileIncludes(text: string): ReadonlyMap<string, {
|
|
53
|
+
readonly taskfile: string;
|
|
54
|
+
readonly optional: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Assert every consumer check gate resolves against `frameworkRoot` Taskfile + includes.
|
|
58
|
+
* When the root Taskfile is absent, returns ok with no findings (caller/spawn handles that).
|
|
59
|
+
*/
|
|
60
|
+
export declare function evaluateConsumerGateIntegrity(frameworkRoot: string, seams?: ConsumerGateIntegritySeams): ConsumerGateIntegrityResult;
|
|
61
|
+
/** Format integrity failure for stderr (check orchestrator / doctor). */
|
|
62
|
+
export declare function formatConsumerGateIntegrityFailure(result: ConsumerGateIntegrityResult): string;
|
|
63
|
+
/**
|
|
64
|
+
* Namespaces that must not be `optional: true` on the root Taskfile when the
|
|
65
|
+
* check graph depends on them — silent omit is the #3070 failure mode.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkGraphOptionalIncludeViolations(rootTaskfileText: string, namespaces?: readonly string[]): readonly string[];
|
|
68
|
+
//# sourceMappingURL=consumer-gate-integrity.d.ts.map
|