@deftai/directive-core 0.67.0 → 0.68.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/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +14 -4
- package/dist/check/orchestrator.js +30 -6
- package/dist/doctor/checks.js +11 -11
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/payload-staleness.js +15 -6
- package/dist/forward-coverage/evaluate.d.ts +55 -0
- package/dist/forward-coverage/evaluate.js +254 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/constants.d.ts +14 -0
- package/dist/init-deposit/constants.js +10 -0
- package/dist/init-deposit/hygiene.d.ts +50 -0
- package/dist/init-deposit/hygiene.js +187 -0
- package/dist/init-deposit/init-deposit.d.ts +1 -0
- package/dist/init-deposit/init-deposit.js +7 -1
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +48 -30
- package/dist/init-deposit/scaffold.d.ts +3 -4
- package/dist/init-deposit/scaffold.js +3 -24
- package/dist/integration-e2e/helpers.d.ts +0 -1
- package/dist/integration-e2e/helpers.js +3 -7
- package/dist/layout/lifecycle-ref.d.ts +15 -0
- package/dist/layout/lifecycle-ref.js +55 -0
- package/dist/platform/agents-md.d.ts +28 -0
- package/dist/platform/agents-md.js +34 -0
- package/dist/policy/resolve.js +56 -46
- package/dist/pr-merge-readiness/compute.d.ts +10 -0
- package/dist/pr-merge-readiness/compute.js +53 -17
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +1 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
- package/dist/pr-merge-readiness/mergeability.js +105 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +8 -2
- package/dist/pr-monitor/monitor.js +35 -6
- package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
- package/dist/pr-wait-mergeable/wrappers.js +13 -5
- package/dist/pr-watch/constants.d.ts +33 -0
- package/dist/pr-watch/constants.js +33 -0
- package/dist/pr-watch/index.d.ts +6 -0
- package/dist/pr-watch/index.js +6 -0
- package/dist/pr-watch/main.d.ts +21 -0
- package/dist/pr-watch/main.js +206 -0
- package/dist/pr-watch/probe.d.ts +13 -0
- package/dist/pr-watch/probe.js +100 -0
- package/dist/pr-watch/types.d.ts +47 -0
- package/dist/pr-watch/types.js +2 -0
- package/dist/pr-watch/watch.d.ts +12 -0
- package/dist/pr-watch/watch.js +122 -0
- package/dist/render/project-render.js +114 -89
- package/dist/scope/capacity-stamp.d.ts +24 -2
- package/dist/scope/capacity-stamp.js +80 -28
- package/dist/scope/decomposed-refs.js +2 -2
- package/dist/scope/project-definition-sync.js +3 -3
- package/dist/scope/vbrief-ref.d.ts +2 -1
- package/dist/scope/vbrief-ref.js +3 -2
- package/dist/spec-authority/constants.d.ts +6 -0
- package/dist/spec-authority/constants.js +11 -0
- package/dist/spec-authority/resolver.js +3 -3
- package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
- package/dist/swarm/finalize-cohort-cli.js +100 -0
- package/dist/swarm/finalize-cohort.d.ts +39 -0
- package/dist/swarm/finalize-cohort.js +589 -0
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/launch.d.ts +7 -0
- package/dist/swarm/launch.js +17 -5
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/project.d.ts +6 -1
- package/dist/triage/queue/project.js +9 -4
- package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
- package/dist/triage/queue/reconcile-live-state.js +54 -0
- package/dist/triage/scope/mutations-core.js +91 -81
- package/dist/triage/scope/resolve.js +4 -3
- package/dist/triage/scope-drift/add-ignore.js +33 -32
- package/dist/triage/scope-drift/scope-rules.js +3 -3
- package/dist/triage/subscribe/index.js +51 -45
- package/dist/triage/welcome/writers.js +79 -64
- package/dist/vbrief-build/project-definition-io.js +8 -3
- package/dist/vbrief-validate/paths.js +8 -4
- package/dist/vbrief-validate/precutover.d.ts +1 -1
- package/dist/vbrief-validate/precutover.js +12 -10
- package/package.json +7 -7
- package/dist/install-upgrade/index.d.ts +0 -13
- package/dist/install-upgrade/index.js +0 -251
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
|
|
4
4
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
5
|
+
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
5
6
|
import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
|
|
6
7
|
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
|
|
7
8
|
import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
|
|
@@ -188,109 +189,133 @@ export function createSkeleton(items, now, layout) {
|
|
|
188
189
|
* (Mirrors ``scripts/project_render.render_project_definition``.)
|
|
189
190
|
*/
|
|
190
191
|
export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
// Serialise the whole read-modify-write of PROJECT-DEFINITION under the shared
|
|
193
|
+
// mutation lock so a concurrent policy/triage mutator cannot be clobbered by the
|
|
194
|
+
// materialised items/metadata write (or vice versa) (#1260).
|
|
195
|
+
return projectDefinitionMutationLock(resolve(vbriefDir, ".."), () => {
|
|
196
|
+
const nowDate = options.now ?? new Date();
|
|
197
|
+
const now = nowDate.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
198
|
+
const layout = resolveProjectDefinitionLayout(vbriefDir);
|
|
199
|
+
const projectDefPath = join(vbriefDir, layout.filename);
|
|
200
|
+
const items = scanLifecycleFolders(vbriefDir);
|
|
201
|
+
const createdNew = !existsSync(projectDefPath);
|
|
202
|
+
let projectDef;
|
|
203
|
+
if (existsSync(projectDefPath)) {
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(readFileSync(projectDefPath, "utf8"));
|
|
207
|
+
}
|
|
208
|
+
catch (exc) {
|
|
209
|
+
return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
|
|
210
|
+
}
|
|
211
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
212
|
+
return [false, `✗ ${projectDefPath} top-level value is not a JSON object`];
|
|
213
|
+
}
|
|
214
|
+
projectDef = parsed;
|
|
215
|
+
const plan = (projectDef.plan ?? {});
|
|
216
|
+
plan.items = items;
|
|
217
|
+
if (typeof projectDef[layout.infoRootKey] !== "object" ||
|
|
218
|
+
projectDef[layout.infoRootKey] === null) {
|
|
219
|
+
projectDef[layout.infoRootKey] = {};
|
|
220
|
+
}
|
|
221
|
+
projectDef[layout.infoRootKey].updated = now;
|
|
222
|
+
const narratives = typeof plan.narratives === "object" &&
|
|
223
|
+
plan.narratives !== null &&
|
|
224
|
+
!Array.isArray(plan.narratives)
|
|
225
|
+
? plan.narratives
|
|
226
|
+
: {};
|
|
227
|
+
const completedItems = items.filter((i) => i.status === "completed");
|
|
228
|
+
if (typeof plan.metadata !== "object" ||
|
|
229
|
+
plan.metadata === null ||
|
|
230
|
+
Array.isArray(plan.metadata)) {
|
|
231
|
+
plan.metadata = {};
|
|
232
|
+
}
|
|
233
|
+
const planMetadata = plan.metadata;
|
|
234
|
+
const review = parseStalenessReview(planMetadata);
|
|
235
|
+
const flags = computeStalenessFlags(narratives, completedItems, review);
|
|
236
|
+
planMetadata.staleness_flags = flags;
|
|
237
|
+
projectDef.plan = plan;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
projectDef = createSkeleton(items, now, layout);
|
|
241
|
+
}
|
|
242
|
+
mkdirSync(vbriefDir, { recursive: true });
|
|
243
|
+
// Atomic temp+rename write under the lock so external readers never observe
|
|
244
|
+
// a partially-written PROJECT-DEFINITION (#1260).
|
|
245
|
+
atomicWriteProjectDefinition(projectDefPath, projectDef);
|
|
246
|
+
const itemCount = items.length;
|
|
247
|
+
const planMeta = (projectDef.plan?.metadata ?? {});
|
|
248
|
+
const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
|
|
249
|
+
const action = createdNew ? "created" : "updated";
|
|
250
|
+
const parts = [`✓ ${layout.filename} ${action} (${itemCount} scope items)`];
|
|
251
|
+
if (flagCount > 0)
|
|
252
|
+
parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
|
|
253
|
+
return [true, parts.join("\n")];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
|
|
258
|
+
*
|
|
259
|
+
* Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
|
|
260
|
+
* vBRIEFs — this path only clears narrative staleness heuristics on render.
|
|
261
|
+
*/
|
|
262
|
+
export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
|
|
263
|
+
// Serialise the read-modify-write of PROJECT-DEFINITION under the shared
|
|
264
|
+
// mutation lock so a concurrent policy/triage mutator is not clobbered (#1260).
|
|
265
|
+
return projectDefinitionMutationLock(resolve(vbriefDir, ".."), () => {
|
|
266
|
+
const nowDate = options.now ?? new Date();
|
|
267
|
+
const now = isoTimestamp(nowDate);
|
|
268
|
+
const layout = resolveProjectDefinitionLayout(vbriefDir);
|
|
269
|
+
const projectDefPath = join(vbriefDir, layout.filename);
|
|
270
|
+
if (!existsSync(projectDefPath)) {
|
|
271
|
+
return [false, `✗ ${projectDefPath} not found — run project:render first`];
|
|
272
|
+
}
|
|
273
|
+
let parsed;
|
|
199
274
|
try {
|
|
200
|
-
|
|
275
|
+
parsed = JSON.parse(readFileSync(projectDefPath, "utf8"));
|
|
201
276
|
}
|
|
202
277
|
catch (exc) {
|
|
203
278
|
return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
|
|
204
279
|
}
|
|
280
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
281
|
+
return [false, `✗ ${projectDefPath} top-level value is not a JSON object`];
|
|
282
|
+
}
|
|
283
|
+
const projectDef = parsed;
|
|
205
284
|
const plan = (projectDef.plan ?? {});
|
|
206
|
-
plan.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
285
|
+
if (typeof plan.metadata !== "object" ||
|
|
286
|
+
plan.metadata === null ||
|
|
287
|
+
Array.isArray(plan.metadata)) {
|
|
288
|
+
plan.metadata = {};
|
|
210
289
|
}
|
|
211
|
-
|
|
290
|
+
const planMetadata = plan.metadata;
|
|
291
|
+
const items = scanLifecycleFolders(vbriefDir);
|
|
292
|
+
const completedItems = items.filter((i) => i.status === "completed");
|
|
293
|
+
const existing = parseStalenessReview(planMetadata);
|
|
294
|
+
planMetadata.staleness_review = buildStalenessAcknowledgement(completedItems, {
|
|
295
|
+
now: nowDate,
|
|
296
|
+
existing,
|
|
297
|
+
});
|
|
212
298
|
const narratives = typeof plan.narratives === "object" &&
|
|
213
299
|
plan.narratives !== null &&
|
|
214
300
|
!Array.isArray(plan.narratives)
|
|
215
301
|
? plan.narratives
|
|
216
302
|
: {};
|
|
217
|
-
|
|
218
|
-
if (typeof
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
plan.metadata = {};
|
|
303
|
+
planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
|
|
304
|
+
if (typeof projectDef[layout.infoRootKey] !== "object" ||
|
|
305
|
+
projectDef[layout.infoRootKey] === null) {
|
|
306
|
+
projectDef[layout.infoRootKey] = {};
|
|
222
307
|
}
|
|
223
|
-
|
|
224
|
-
const review = parseStalenessReview(planMetadata);
|
|
225
|
-
const flags = computeStalenessFlags(narratives, completedItems, review);
|
|
226
|
-
planMetadata.staleness_flags = flags;
|
|
308
|
+
projectDef[layout.infoRootKey].updated = now;
|
|
227
309
|
projectDef.plan = plan;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
|
|
237
|
-
const action = createdNew ? "created" : "updated";
|
|
238
|
-
const parts = [`✓ ${layout.filename} ${action} (${itemCount} scope items)`];
|
|
239
|
-
if (flagCount > 0)
|
|
240
|
-
parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
|
|
241
|
-
return [true, parts.join("\n")];
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
|
|
245
|
-
*
|
|
246
|
-
* Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
|
|
247
|
-
* vBRIEFs — this path only clears narrative staleness heuristics on render.
|
|
248
|
-
*/
|
|
249
|
-
export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
|
|
250
|
-
const nowDate = options.now ?? new Date();
|
|
251
|
-
const now = isoTimestamp(nowDate);
|
|
252
|
-
const layout = resolveProjectDefinitionLayout(vbriefDir);
|
|
253
|
-
const projectDefPath = join(vbriefDir, layout.filename);
|
|
254
|
-
if (!existsSync(projectDefPath)) {
|
|
255
|
-
return [false, `✗ ${projectDefPath} not found — run project:render first`];
|
|
256
|
-
}
|
|
257
|
-
let projectDef;
|
|
258
|
-
try {
|
|
259
|
-
projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
|
|
260
|
-
}
|
|
261
|
-
catch (exc) {
|
|
262
|
-
return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
|
|
263
|
-
}
|
|
264
|
-
const plan = (projectDef.plan ?? {});
|
|
265
|
-
if (typeof plan.metadata !== "object" || plan.metadata === null || Array.isArray(plan.metadata)) {
|
|
266
|
-
plan.metadata = {};
|
|
267
|
-
}
|
|
268
|
-
const planMetadata = plan.metadata;
|
|
269
|
-
const items = scanLifecycleFolders(vbriefDir);
|
|
270
|
-
const completedItems = items.filter((i) => i.status === "completed");
|
|
271
|
-
const existing = parseStalenessReview(planMetadata);
|
|
272
|
-
planMetadata.staleness_review = buildStalenessAcknowledgement(completedItems, {
|
|
273
|
-
now: nowDate,
|
|
274
|
-
existing,
|
|
310
|
+
// Atomic temp+rename write under the lock so external readers never observe
|
|
311
|
+
// a partially-written PROJECT-DEFINITION (#1260).
|
|
312
|
+
atomicWriteProjectDefinition(projectDefPath, projectDef);
|
|
313
|
+
const ackCount = completedItems.length;
|
|
314
|
+
return [
|
|
315
|
+
true,
|
|
316
|
+
`✓ PROJECT-DEFINITION staleness acknowledged (${ackCount} completed scope(s) watermarked)`,
|
|
317
|
+
];
|
|
275
318
|
});
|
|
276
|
-
const narratives = typeof plan.narratives === "object" &&
|
|
277
|
-
plan.narratives !== null &&
|
|
278
|
-
!Array.isArray(plan.narratives)
|
|
279
|
-
? plan.narratives
|
|
280
|
-
: {};
|
|
281
|
-
planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
|
|
282
|
-
if (typeof projectDef[layout.infoRootKey] !== "object" ||
|
|
283
|
-
projectDef[layout.infoRootKey] === null) {
|
|
284
|
-
projectDef[layout.infoRootKey] = {};
|
|
285
|
-
}
|
|
286
|
-
projectDef[layout.infoRootKey].updated = now;
|
|
287
|
-
projectDef.plan = plan;
|
|
288
|
-
writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
|
|
289
|
-
const ackCount = completedItems.length;
|
|
290
|
-
return [
|
|
291
|
-
true,
|
|
292
|
-
`✓ PROJECT-DEFINITION staleness acknowledged (${ackCount} completed scope(s) watermarked)`,
|
|
293
|
-
];
|
|
294
319
|
}
|
|
295
320
|
/** CLI entry (mirrors ``scripts/project_render.main``). */
|
|
296
321
|
export function main(argv) {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Read the labels for an issue reference. Returns null when they cannot be resolved. */
|
|
2
|
+
export type LabelReader = (repo: string, issueNumber: number) => ReadonlySet<string> | null;
|
|
3
|
+
export interface StampCompletionOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Explicit label set for the completing brief. When provided it takes precedence
|
|
6
|
+
* over any linked-issue lookup, keeping bucket resolution pure and network-free.
|
|
7
|
+
*/
|
|
8
|
+
readonly labels?: Iterable<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Injectable reader used to resolve labels from the brief's linked issue when no
|
|
11
|
+
* explicit `labels` are supplied. Defaults to a cached-issue lookup (no network).
|
|
12
|
+
* A `null` return signals a cache MISS and triggers the live fallback below.
|
|
13
|
+
*/
|
|
14
|
+
readonly labelReader?: LabelReader;
|
|
15
|
+
/**
|
|
16
|
+
* Injectable fail-open fallback used ONLY on a cache miss (`labelReader` returned
|
|
17
|
+
* `null`). Defaults to a single REST read via the scm shim (PRs excluded, #2246).
|
|
18
|
+
* A `null` return leaves resolution on the `defaultBucket` fallback. Injected in
|
|
19
|
+
* tests so the fallback path is exercised without a network call.
|
|
20
|
+
*/
|
|
21
|
+
readonly liveLabelReader?: LabelReader;
|
|
22
|
+
}
|
|
23
|
+
/** Stamp completedAt + label-matched capacityBucket onto a completing vBRIEF (#1419, #2237). */
|
|
24
|
+
export declare function stampCompletionMetadata(plan: Record<string, unknown>, projectRoot: string, timestamp: string, options?: StampCompletionOptions): void;
|
|
3
25
|
//# sourceMappingURL=capacity-stamp.d.ts.map
|
|
@@ -1,33 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const plan = data.plan;
|
|
11
|
-
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
12
|
-
return "";
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { cachedIssueLabels, extractIssueRef } from "../capacity/backfill.js";
|
|
3
|
+
import { classifyBucket, loadBucketMatchers } from "../policy/capacity.js";
|
|
4
|
+
import { restIssueView } from "../scm/gh-rest.js";
|
|
5
|
+
function normalizeLabels(labels) {
|
|
6
|
+
const out = new Set();
|
|
7
|
+
for (const label of labels) {
|
|
8
|
+
if (typeof label === "string" && label.trim().length > 0) {
|
|
9
|
+
out.add(label);
|
|
13
10
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** Extract label name strings from a raw REST/cache `labels` array. */
|
|
15
|
+
function labelsFromRaw(raw) {
|
|
16
|
+
const out = new Set();
|
|
17
|
+
if (!Array.isArray(raw)) {
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
for (const label of raw) {
|
|
21
|
+
if (typeof label === "string" && label.length > 0) {
|
|
22
|
+
out.add(label);
|
|
17
23
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
else if (typeof label === "object" && label !== null && !Array.isArray(label)) {
|
|
25
|
+
const name = label.name;
|
|
26
|
+
if (typeof name === "string" && name.length > 0) {
|
|
27
|
+
out.add(name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fail-open live label read for a cache miss: a single REST GET through the scm
|
|
35
|
+
* shim (`restIssueView`), with PRs excluded. Any error (or a PR ref) yields `null`
|
|
36
|
+
* so resolution falls through to `defaultBucket` instead of crashing (#2246).
|
|
37
|
+
*/
|
|
38
|
+
function liveIssueLabels(repo, issueNumber) {
|
|
39
|
+
try {
|
|
40
|
+
const issue = restIssueView(repo, issueNumber);
|
|
41
|
+
// The issues endpoint also returns PRs; a PR object carries `pull_request`.
|
|
42
|
+
// Exclude PRs from label-based bucket resolution.
|
|
43
|
+
if (issue.pull_request !== undefined) {
|
|
44
|
+
return null;
|
|
21
45
|
}
|
|
22
|
-
|
|
23
|
-
return typeof defaultBucket === "string" ? defaultBucket : "";
|
|
46
|
+
return labelsFromRaw(issue.labels);
|
|
24
47
|
}
|
|
25
48
|
catch {
|
|
26
|
-
return
|
|
49
|
+
return null;
|
|
27
50
|
}
|
|
28
51
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the capacityBucket for a completing brief by matching its issue labels
|
|
54
|
+
* against `capacityAllocation.buckets[].match.labels` (first declared match wins),
|
|
55
|
+
* falling back to `defaultBucket` when nothing matches (#2237).
|
|
56
|
+
*/
|
|
57
|
+
function resolveCapacityBucket(plan, projectRoot, options) {
|
|
58
|
+
const root = resolve(projectRoot);
|
|
59
|
+
const { matchers, default_bucket: defaultBucket } = loadBucketMatchers(root);
|
|
60
|
+
let labels = null;
|
|
61
|
+
if (options.labels !== undefined) {
|
|
62
|
+
labels = normalizeLabels(options.labels);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const [repo, issueNumber] = extractIssueRef(plan);
|
|
66
|
+
if (repo !== null && issueNumber !== null) {
|
|
67
|
+
const reader = options.labelReader ?? ((r, n) => cachedIssueLabels(root, r, n));
|
|
68
|
+
labels = reader(repo, issueNumber);
|
|
69
|
+
// Cache MISS (null) -- take the fail-open live fallback. A cache HIT (even an
|
|
70
|
+
// empty set) stays the fast path and makes NO network call (#2246).
|
|
71
|
+
if (labels === null) {
|
|
72
|
+
const live = options.liveLabelReader ?? liveIssueLabels;
|
|
73
|
+
labels = live(repo, issueNumber);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const [bucket] = classifyBucket(labels ?? new Set(), matchers, defaultBucket);
|
|
78
|
+
return bucket;
|
|
79
|
+
}
|
|
80
|
+
/** Stamp completedAt + label-matched capacityBucket onto a completing vBRIEF (#1419, #2237). */
|
|
81
|
+
export function stampCompletionMetadata(plan, projectRoot, timestamp, options = {}) {
|
|
31
82
|
let metadata = plan.metadata;
|
|
32
83
|
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
|
|
33
84
|
metadata = {};
|
|
@@ -36,11 +87,12 @@ export function stampCompletionMetadata(plan, projectRoot, timestamp) {
|
|
|
36
87
|
const meta = metadata;
|
|
37
88
|
meta.completedAt = timestamp;
|
|
38
89
|
const existing = meta.capacityBucket;
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
if (typeof existing === "string" && existing.trim().length > 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const bucket = resolveCapacityBucket(plan, projectRoot, options);
|
|
94
|
+
if (bucket.length > 0) {
|
|
95
|
+
meta.capacityBucket = bucket;
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
98
|
//# sourceMappingURL=capacity-stamp.js.map
|
|
@@ -7,7 +7,7 @@ function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
|
|
|
7
7
|
if (typeof value !== "string" || value.length === 0) {
|
|
8
8
|
return [value, false];
|
|
9
9
|
}
|
|
10
|
-
const resolved = resolveVbriefRef(value, vbriefDir);
|
|
10
|
+
const resolved = resolveVbriefRef(value, vbriefDir, { allowCrossFolderSearch: false });
|
|
11
11
|
if (resolved === null || resolve(resolved) !== resolve(oldParentResolved)) {
|
|
12
12
|
return [value, false];
|
|
13
13
|
}
|
|
@@ -40,7 +40,7 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
|
|
|
40
40
|
if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
const resolved = resolveVbriefRef(r.uri, vbriefDir);
|
|
43
|
+
const resolved = resolveVbriefRef(r.uri, vbriefDir, { allowCrossFolderSearch: false });
|
|
44
44
|
if (resolved === null || resolve(resolved) !== resolve(oldChildResolved)) {
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname,
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
5
5
|
import { formatVbriefJson } from "./vbrief-json.js";
|
|
6
6
|
import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
|
|
7
7
|
function rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot) {
|
|
@@ -95,7 +95,7 @@ export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath,
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const projectRoot = dirname(resolve(vbriefRoot));
|
|
98
|
-
const projectDefPath =
|
|
98
|
+
const projectDefPath = resolveProjectDefinitionPath(projectRoot);
|
|
99
99
|
if (!existsSync(projectDefPath)) {
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ResolveLifecycleArtifactRefOptions } from "../layout/lifecycle-ref.js";
|
|
1
2
|
/** Resolve a vBRIEF reference URI to an absolute path, or null. */
|
|
2
|
-
export declare function resolveVbriefRef(uri: unknown, vbriefDir: string): string | null;
|
|
3
|
+
export declare function resolveVbriefRef(uri: unknown, vbriefDir: string, options?: ResolveLifecycleArtifactRefOptions): string | null;
|
|
3
4
|
/** Collect planRef values from the plan root and top-level items. */
|
|
4
5
|
export declare function collectPlanRefs(plan: Record<string, unknown>): string[];
|
|
5
6
|
/** Collect x-vbrief/plan child reference uris from a parent plan. */
|
package/dist/scope/vbrief-ref.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
3
|
+
import { resolveLifecycleArtifactRef, } from "../layout/lifecycle-ref.js";
|
|
3
4
|
import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
|
|
4
5
|
/** Resolve a vBRIEF reference URI to an absolute path, or null. */
|
|
5
|
-
export function resolveVbriefRef(uri, vbriefDir) {
|
|
6
|
+
export function resolveVbriefRef(uri, vbriefDir, options = {}) {
|
|
6
7
|
if (typeof uri !== "string" || uri.length === 0) {
|
|
7
8
|
return null;
|
|
8
9
|
}
|
|
@@ -16,7 +17,7 @@ export function resolveVbriefRef(uri, vbriefDir) {
|
|
|
16
17
|
else {
|
|
17
18
|
rel = uri;
|
|
18
19
|
}
|
|
19
|
-
return
|
|
20
|
+
return resolveLifecycleArtifactRef(rel, vbriefDir, options);
|
|
20
21
|
}
|
|
21
22
|
/** Collect planRef values from the plan root and top-level items. */
|
|
22
23
|
export function collectPlanRefs(plan) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
|
|
2
2
|
export declare const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
3
3
|
export declare const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
4
|
+
export declare const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
|
|
4
5
|
export declare const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
|
|
6
|
+
export declare const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
|
|
7
|
+
/** True when markdown carries a generated-spec source marker for either layout. */
|
|
8
|
+
export declare function contentHasGeneratedSpecSource(content: string): boolean;
|
|
9
|
+
/** True when markdown carries a generated greenfield PD source marker for either layout. */
|
|
10
|
+
export declare function contentHasGeneratedPdSource(content: string): boolean;
|
|
5
11
|
export declare const EXPORT_SPEC_PD_BANNER: string;
|
|
6
12
|
/** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
|
|
7
13
|
export declare const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS: Set<string>;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
|
|
2
2
|
export const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
3
3
|
export const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
4
|
+
export const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
|
|
4
5
|
export const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
|
|
6
|
+
export const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
|
|
7
|
+
/** True when markdown carries a generated-spec source marker for either layout. */
|
|
8
|
+
export function contentHasGeneratedSpecSource(content) {
|
|
9
|
+
return (content.includes(GENERATED_SPEC_SOURCE_SPEC) ||
|
|
10
|
+
content.includes(GENERATED_SPEC_SOURCE_SPEC_XBRIEF));
|
|
11
|
+
}
|
|
12
|
+
/** True when markdown carries a generated greenfield PD source marker for either layout. */
|
|
13
|
+
export function contentHasGeneratedPdSource(content) {
|
|
14
|
+
return (content.includes(GENERATED_SPEC_SOURCE_PD) || content.includes(GENERATED_SPEC_SOURCE_PD_XBRIEF));
|
|
15
|
+
}
|
|
5
16
|
export const EXPORT_SPEC_PD_BANNER = "<!-- AUTO-GENERATED by task project:export-spec -- DO NOT EDIT MANUALLY -->\n" +
|
|
6
17
|
`${GENERATED_SPEC_PURPOSE}\n` +
|
|
7
18
|
`${GENERATED_SPEC_SOURCE_PD}\n` +
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../layout/resolve.js";
|
|
4
4
|
import { SPEC_RENDER_BANNER } from "../render/constants.js";
|
|
5
|
-
import {
|
|
5
|
+
import { contentHasGeneratedPdSource, contentHasGeneratedSpecSource, EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, } from "./constants.js";
|
|
6
6
|
export function resolveSpecAuthority(projectRoot) {
|
|
7
7
|
const root = projectRoot;
|
|
8
8
|
const layout = resolveLifecycleLayout(root);
|
|
@@ -37,14 +37,14 @@ export function isFullSpecState(projectRoot) {
|
|
|
37
37
|
if (authority?.kind !== "full-spec")
|
|
38
38
|
return false;
|
|
39
39
|
const specMd = readSpecMarkdown(projectRoot);
|
|
40
|
-
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd
|
|
40
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && contentHasGeneratedSpecSource(specMd);
|
|
41
41
|
}
|
|
42
42
|
export function isGreenfieldSpecExport(projectRoot) {
|
|
43
43
|
const authority = resolveSpecAuthority(projectRoot);
|
|
44
44
|
if (authority?.kind !== "greenfield")
|
|
45
45
|
return false;
|
|
46
46
|
const specMd = readSpecMarkdown(projectRoot);
|
|
47
|
-
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd
|
|
47
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && contentHasGeneratedPdSource(specMd);
|
|
48
48
|
}
|
|
49
49
|
export function isCurrentGeneratedSpecification(projectRoot) {
|
|
50
50
|
return isFullSpecState(projectRoot) || isGreenfieldSpecExport(projectRoot);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { finalizeCohort } from "./finalize-cohort.js";
|
|
3
|
+
export declare function parseFinalizeCohortArgv(argv: readonly string[]): Parameters<typeof finalizeCohort>[0];
|
|
4
|
+
export declare function finalizeCohortMain(argv?: string[]): number;
|
|
5
|
+
//# sourceMappingURL=finalize-cohort-cli.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { finalizeCohort } from "./finalize-cohort.js";
|
|
4
|
+
export function parseFinalizeCohortArgv(argv) {
|
|
5
|
+
const prNumbers = [];
|
|
6
|
+
const storyTokens = [];
|
|
7
|
+
let repo = null;
|
|
8
|
+
let projectRoot = ".";
|
|
9
|
+
let baseBranch = "master";
|
|
10
|
+
let label = null;
|
|
11
|
+
let dryRun = false;
|
|
12
|
+
let noCommit = false;
|
|
13
|
+
let noOpenPr = false;
|
|
14
|
+
let emitJson = false;
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (arg === "--pr" && next !== undefined) {
|
|
19
|
+
for (const piece of next.split(",")) {
|
|
20
|
+
const trimmed = piece.trim();
|
|
21
|
+
if (/^\d+$/.test(trimmed)) {
|
|
22
|
+
prNumbers.push(Number.parseInt(trimmed, 10));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (arg?.startsWith("--pr=")) {
|
|
28
|
+
for (const piece of arg.slice("--pr=".length).split(",")) {
|
|
29
|
+
const trimmed = piece.trim();
|
|
30
|
+
if (/^\d+$/.test(trimmed)) {
|
|
31
|
+
prNumbers.push(Number.parseInt(trimmed, 10));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--stories" && next !== undefined) {
|
|
36
|
+
storyTokens.push(next);
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--repo" && next !== undefined) {
|
|
40
|
+
repo = next;
|
|
41
|
+
i += 1;
|
|
42
|
+
}
|
|
43
|
+
else if (arg?.startsWith("--repo=")) {
|
|
44
|
+
repo = arg.slice("--repo=".length);
|
|
45
|
+
}
|
|
46
|
+
else if (arg === "--project-root" && next !== undefined) {
|
|
47
|
+
projectRoot = next;
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
else if (arg === "--base-branch" && next !== undefined) {
|
|
51
|
+
baseBranch = next;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === "--label" && next !== undefined) {
|
|
55
|
+
label = next;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--dry-run") {
|
|
59
|
+
dryRun = true;
|
|
60
|
+
}
|
|
61
|
+
else if (arg === "--no-commit") {
|
|
62
|
+
noCommit = true;
|
|
63
|
+
}
|
|
64
|
+
else if (arg === "--no-open-pr") {
|
|
65
|
+
noOpenPr = true;
|
|
66
|
+
}
|
|
67
|
+
else if (arg === "--json") {
|
|
68
|
+
emitJson = true;
|
|
69
|
+
}
|
|
70
|
+
else if (arg !== undefined && !arg.startsWith("-")) {
|
|
71
|
+
storyTokens.push(arg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
prNumbers,
|
|
76
|
+
storyTokens,
|
|
77
|
+
repo,
|
|
78
|
+
projectRoot,
|
|
79
|
+
baseBranch,
|
|
80
|
+
label,
|
|
81
|
+
dryRun,
|
|
82
|
+
noCommit,
|
|
83
|
+
noOpenPr,
|
|
84
|
+
emitJson,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function finalizeCohortMain(argv = process.argv.slice(2)) {
|
|
88
|
+
const result = finalizeCohort(parseFinalizeCohortArgv(argv));
|
|
89
|
+
if (result.stdout.length > 0) {
|
|
90
|
+
process.stdout.write(result.stdout);
|
|
91
|
+
}
|
|
92
|
+
if (result.stderr.length > 0) {
|
|
93
|
+
process.stderr.write(result.stderr);
|
|
94
|
+
}
|
|
95
|
+
return result.exitCode;
|
|
96
|
+
}
|
|
97
|
+
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
98
|
+
process.exit(finalizeCohortMain());
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=finalize-cohort-cli.js.map
|