@deftai/directive-core 0.67.0 → 0.68.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/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 +176 -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
package/dist/swarm/launch.js
CHANGED
|
@@ -126,17 +126,17 @@ function projectRel(projectRoot, path) {
|
|
|
126
126
|
return path.replace(/\\/g, "/");
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
129
|
+
function indexStoriesInFolder(projectRoot, folder) {
|
|
130
|
+
const dir = resolveLifecycleFolder(projectRoot, folder);
|
|
131
131
|
const index = [];
|
|
132
|
-
if (!existsSync(
|
|
132
|
+
if (!existsSync(dir)) {
|
|
133
133
|
return index;
|
|
134
134
|
}
|
|
135
|
-
for (const name of readdirSync(
|
|
135
|
+
for (const name of readdirSync(dir).sort()) {
|
|
136
136
|
if (!hasArtifactSuffix(name)) {
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
|
-
const path = join(
|
|
139
|
+
const path = join(dir, name);
|
|
140
140
|
const data = loadJson(path);
|
|
141
141
|
if (data === null) {
|
|
142
142
|
continue;
|
|
@@ -146,6 +146,18 @@ function indexActiveStories(projectRoot) {
|
|
|
146
146
|
}
|
|
147
147
|
return index;
|
|
148
148
|
}
|
|
149
|
+
function indexActiveStories(projectRoot) {
|
|
150
|
+
return indexStoriesInFolder(projectRoot, "active");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* True when a brief in `xbrief/completed/` (or the legacy `vbrief/completed/`)
|
|
154
|
+
* references the given issue number in its `references[]` / `Traces`. Used by
|
|
155
|
+
* finalize-cohort (#2247) to classify an incidental closing ref to an
|
|
156
|
+
* already-completed issue as a benign skip rather than a hard error.
|
|
157
|
+
*/
|
|
158
|
+
export function completedBriefReferencesIssue(projectRoot, issue) {
|
|
159
|
+
return indexStoriesInFolder(projectRoot, "completed").some((s) => s.issues.has(issue));
|
|
160
|
+
}
|
|
149
161
|
export function looksLikePath(token) {
|
|
150
162
|
return (token.endsWith(".json") ||
|
|
151
163
|
token.includes("/") ||
|
|
@@ -4,6 +4,7 @@ export * from "./cache.js";
|
|
|
4
4
|
export * from "./constants.js";
|
|
5
5
|
export * from "./derive-group.js";
|
|
6
6
|
export * from "./ranking-labels.js";
|
|
7
|
+
export * from "./reconcile-live-state.js";
|
|
7
8
|
export * from "./render.js";
|
|
8
9
|
export * from "./repo.js";
|
|
9
10
|
export * from "./scope-ignores-filter.js";
|
|
@@ -4,6 +4,7 @@ export * from "./cache.js";
|
|
|
4
4
|
export * from "./constants.js";
|
|
5
5
|
export * from "./derive-group.js";
|
|
6
6
|
export * from "./ranking-labels.js";
|
|
7
|
+
export * from "./reconcile-live-state.js";
|
|
7
8
|
export * from "./render.js";
|
|
8
9
|
export * from "./repo.js";
|
|
9
10
|
export * from "./scope-ignores-filter.js";
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export interface ProjectDefinition {
|
|
2
2
|
readonly plan?: unknown;
|
|
3
3
|
}
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Read the layout-resolved PROJECT-DEFINITION artifact (#2207). Resolves the
|
|
6
|
+
* `xbrief/` layout on migrated trees and falls back to `vbrief/` otherwise, so
|
|
7
|
+
* `plan.policy.triageRankingLabels` load correctly after the #2109 rename.
|
|
8
|
+
* Returns null if absent/invalid.
|
|
9
|
+
*/
|
|
5
10
|
export declare function loadProjectDefinition(projectRoot: string): ProjectDefinition | null;
|
|
6
11
|
//# sourceMappingURL=project.d.ts.map
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
/**
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read the layout-resolved PROJECT-DEFINITION artifact (#2207). Resolves the
|
|
6
|
+
* `xbrief/` layout on migrated trees and falls back to `vbrief/` otherwise, so
|
|
7
|
+
* `plan.policy.triageRankingLabels` load correctly after the #2109 rename.
|
|
8
|
+
* Returns null if absent/invalid.
|
|
9
|
+
*/
|
|
5
10
|
export function loadProjectDefinition(projectRoot) {
|
|
6
|
-
const path =
|
|
11
|
+
const path = resolveProjectDefinitionPath(resolve(projectRoot));
|
|
7
12
|
if (!existsSync(path)) {
|
|
8
13
|
return null;
|
|
9
14
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CachedIssue } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the set of issue numbers that are currently OPEN on the live SCM.
|
|
4
|
+
*
|
|
5
|
+
* Returns `null` when the live state could not be determined (offline, gh
|
|
6
|
+
* missing / unauthenticated, REST error). Callers MUST treat `null` as "do not
|
|
7
|
+
* reconcile" and fall back to the cached view -- a transient read failure must
|
|
8
|
+
* never empty the queue.
|
|
9
|
+
*/
|
|
10
|
+
export type LiveOpenIssuesReader = (repo: string) => ReadonlySet<number> | null;
|
|
11
|
+
/**
|
|
12
|
+
* Default reader: one batched, paginated REST list (`state=open`, PRs excluded)
|
|
13
|
+
* routed through the scm gh-rest shim. Prefers REST over GraphQL and issues a
|
|
14
|
+
* single logical lookup for the whole open set rather than N per-issue calls.
|
|
15
|
+
* Any failure is swallowed to `null` so the queue degrades to cached rendering.
|
|
16
|
+
*
|
|
17
|
+
* Latency ceiling: no explicit `limit` is passed ON PURPOSE. The reconcile drops
|
|
18
|
+
* any candidate absent from this set, so a truncated page would incorrectly drop
|
|
19
|
+
* genuinely-open issues (a false-negative is worse than the latency). The set is
|
|
20
|
+
* therefore the COMPLETE live-open list, bounded only by `restIssueListPaginated`'s
|
|
21
|
+
* pagination cap (`REST_PAGINATION_MAX_PAGES` * per-page = 100 * 100 = 10,000 open
|
|
22
|
+
* issues); a repo with more open issues than that raises rather than silently
|
|
23
|
+
* truncating. In practice open-issue counts are 10^1-10^2, so this is 1-2 REST
|
|
24
|
+
* pages. Batch here, never per-candidate.
|
|
25
|
+
*/
|
|
26
|
+
export declare function defaultLiveOpenIssuesReader(repo: string): ReadonlySet<number> | null;
|
|
27
|
+
/**
|
|
28
|
+
* Reconcile cached candidate issues against live open/closed state before the
|
|
29
|
+
* queue renders (#2238). The cached candidate set records `state`, but that
|
|
30
|
+
* value goes stale as soon as an issue is closed/merged and the cache has not
|
|
31
|
+
* yet refreshed -- so honoring the cached flag alone is insufficient.
|
|
32
|
+
*
|
|
33
|
+
* Drops any candidate whose number is NOT in the live-open set. When the reader
|
|
34
|
+
* returns `null` (state undeterminable) the candidates pass through unchanged so
|
|
35
|
+
* a network / auth failure never silently empties the queue.
|
|
36
|
+
*
|
|
37
|
+
* `reader` defaults to {@link defaultLiveOpenIssuesReader}; tests (and the CLI)
|
|
38
|
+
* inject a stub via this optional parameter -- there is no module-level mutable
|
|
39
|
+
* seam, so nothing test-only leaks into the package's public API.
|
|
40
|
+
*/
|
|
41
|
+
export declare function reconcileLiveOpenState(issues: readonly CachedIssue[], repo: string, reader?: LiveOpenIssuesReader): readonly CachedIssue[];
|
|
42
|
+
//# sourceMappingURL=reconcile-live-state.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { restIssueListPaginated } from "../../scm/gh-rest.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default reader: one batched, paginated REST list (`state=open`, PRs excluded)
|
|
4
|
+
* routed through the scm gh-rest shim. Prefers REST over GraphQL and issues a
|
|
5
|
+
* single logical lookup for the whole open set rather than N per-issue calls.
|
|
6
|
+
* Any failure is swallowed to `null` so the queue degrades to cached rendering.
|
|
7
|
+
*
|
|
8
|
+
* Latency ceiling: no explicit `limit` is passed ON PURPOSE. The reconcile drops
|
|
9
|
+
* any candidate absent from this set, so a truncated page would incorrectly drop
|
|
10
|
+
* genuinely-open issues (a false-negative is worse than the latency). The set is
|
|
11
|
+
* therefore the COMPLETE live-open list, bounded only by `restIssueListPaginated`'s
|
|
12
|
+
* pagination cap (`REST_PAGINATION_MAX_PAGES` * per-page = 100 * 100 = 10,000 open
|
|
13
|
+
* issues); a repo with more open issues than that raises rather than silently
|
|
14
|
+
* truncating. In practice open-issue counts are 10^1-10^2, so this is 1-2 REST
|
|
15
|
+
* pages. Batch here, never per-candidate.
|
|
16
|
+
*/
|
|
17
|
+
export function defaultLiveOpenIssuesReader(repo) {
|
|
18
|
+
try {
|
|
19
|
+
const rows = restIssueListPaginated(repo, { state: "open", excludePulls: true });
|
|
20
|
+
const open = new Set();
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
const n = row.number;
|
|
23
|
+
if (typeof n === "number" && Number.isInteger(n)) {
|
|
24
|
+
open.add(n);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return open;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile cached candidate issues against live open/closed state before the
|
|
35
|
+
* queue renders (#2238). The cached candidate set records `state`, but that
|
|
36
|
+
* value goes stale as soon as an issue is closed/merged and the cache has not
|
|
37
|
+
* yet refreshed -- so honoring the cached flag alone is insufficient.
|
|
38
|
+
*
|
|
39
|
+
* Drops any candidate whose number is NOT in the live-open set. When the reader
|
|
40
|
+
* returns `null` (state undeterminable) the candidates pass through unchanged so
|
|
41
|
+
* a network / auth failure never silently empties the queue.
|
|
42
|
+
*
|
|
43
|
+
* `reader` defaults to {@link defaultLiveOpenIssuesReader}; tests (and the CLI)
|
|
44
|
+
* inject a stub via this optional parameter -- there is no module-level mutable
|
|
45
|
+
* seam, so nothing test-only leaks into the package's public API.
|
|
46
|
+
*/
|
|
47
|
+
export function reconcileLiveOpenState(issues, repo, reader = defaultLiveOpenIssuesReader) {
|
|
48
|
+
const liveOpen = reader(repo);
|
|
49
|
+
if (liveOpen === null) {
|
|
50
|
+
return issues;
|
|
51
|
+
}
|
|
52
|
+
return issues.filter((issue) => liveOpen.has(issue.number));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=reconcile-live-state.js.map
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
6
6
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
7
|
+
import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
|
|
7
8
|
import { SUBSCRIPTION_HISTORY_SCHEMA } from "./constants.js";
|
|
8
9
|
import { pyStrRepr } from "./python-repr.js";
|
|
9
10
|
import { utcIso } from "./time.js";
|
|
@@ -128,93 +129,102 @@ export function subscribe(projectRoot, options) {
|
|
|
128
129
|
if (chosen.length !== 1) {
|
|
129
130
|
throw new Error(`subscribe() requires exactly one of --label / --milestone / --issue; got ${JSON.stringify(chosen)}`);
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
132
|
+
// Serialise the read-modify-write + subscription-history append under the
|
|
133
|
+
// shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
|
|
134
|
+
// an update or emit out-of-order audit rows (#1260).
|
|
135
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
136
|
+
const [data, path] = loadProjectDefinitionForMutation(projectRoot);
|
|
137
|
+
const plan = data.plan;
|
|
138
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
139
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
|
|
140
|
+
}
|
|
141
|
+
const planRec = plan;
|
|
142
|
+
migrateLegacyPolicyKey(planRec);
|
|
143
|
+
if (planRec[PLAN_POLICY_KEY] === undefined)
|
|
144
|
+
planRec[PLAN_POLICY_KEY] = {};
|
|
145
|
+
const policy = planRec[PLAN_POLICY_KEY];
|
|
146
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
147
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
|
|
148
|
+
}
|
|
149
|
+
const policyRec = policy;
|
|
150
|
+
if (policyRec.triageScope === undefined)
|
|
151
|
+
policyRec.triageScope = [];
|
|
152
|
+
const rules = policyRec.triageScope;
|
|
153
|
+
if (!Array.isArray(rules)) {
|
|
154
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
|
|
155
|
+
}
|
|
156
|
+
const before = snapshotRules(rules);
|
|
157
|
+
let changed;
|
|
158
|
+
let message;
|
|
159
|
+
if (options.label !== undefined) {
|
|
160
|
+
[changed, message] = applySubscribeLabel(rules, options.label);
|
|
161
|
+
}
|
|
162
|
+
else if (options.milestone !== undefined) {
|
|
163
|
+
[changed, message] = applySubscribeMilestone(rules, options.milestone);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
throw new Error("subscribe() requires exactly one of label or milestone");
|
|
167
|
+
}
|
|
168
|
+
if (!changed)
|
|
169
|
+
return [false, message];
|
|
170
|
+
atomicWriteProjectDefinition(path, data);
|
|
171
|
+
recordSubscriptionChange(projectRoot, {
|
|
172
|
+
op: "subscribe",
|
|
173
|
+
label: options.label ?? null,
|
|
174
|
+
milestone: options.milestone ?? null,
|
|
175
|
+
before,
|
|
176
|
+
after: snapshotRules(rules),
|
|
177
|
+
actor: options.actor,
|
|
178
|
+
});
|
|
179
|
+
return [true, message];
|
|
173
180
|
});
|
|
174
|
-
return [true, message];
|
|
175
181
|
}
|
|
176
182
|
export function addIgnore(projectRoot, label) {
|
|
177
183
|
if (!label.trim())
|
|
178
184
|
throw new Error(`label must be a non-empty string; got ${JSON.stringify(label)}`);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (planRec[PLAN_POLICY_KEY] === undefined)
|
|
187
|
-
planRec[PLAN_POLICY_KEY] = {};
|
|
188
|
-
const policy = planRec[PLAN_POLICY_KEY];
|
|
189
|
-
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
190
|
-
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
|
|
191
|
-
}
|
|
192
|
-
const policyRec = policy;
|
|
193
|
-
if (policyRec.triageScopeIgnores === undefined)
|
|
194
|
-
policyRec.triageScopeIgnores = [];
|
|
195
|
-
const raw = policyRec.triageScopeIgnores;
|
|
196
|
-
if (!Array.isArray(raw)) {
|
|
197
|
-
throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScopeIgnores'`);
|
|
198
|
-
}
|
|
199
|
-
const before = snapshotRules(raw);
|
|
200
|
-
for (const entry of raw) {
|
|
201
|
-
if (typeof entry === "object" &&
|
|
202
|
-
entry !== null &&
|
|
203
|
-
!Array.isArray(entry) &&
|
|
204
|
-
entry.label === label) {
|
|
205
|
-
return [false, `already-ignored (label=${label})`];
|
|
185
|
+
// Serialise the read-modify-write + subscription-history append under the
|
|
186
|
+
// shared PROJECT-DEFINITION mutation lock (#1260).
|
|
187
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
188
|
+
const [data, path] = loadProjectDefinitionForMutation(projectRoot);
|
|
189
|
+
const plan = data.plan;
|
|
190
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
191
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
|
|
206
192
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
193
|
+
const planRec = plan;
|
|
194
|
+
migrateLegacyPolicyKey(planRec);
|
|
195
|
+
if (planRec[PLAN_POLICY_KEY] === undefined)
|
|
196
|
+
planRec[PLAN_POLICY_KEY] = {};
|
|
197
|
+
const policy = planRec[PLAN_POLICY_KEY];
|
|
198
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
199
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
|
|
200
|
+
}
|
|
201
|
+
const policyRec = policy;
|
|
202
|
+
if (policyRec.triageScopeIgnores === undefined)
|
|
203
|
+
policyRec.triageScopeIgnores = [];
|
|
204
|
+
const raw = policyRec.triageScopeIgnores;
|
|
205
|
+
if (!Array.isArray(raw)) {
|
|
206
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScopeIgnores'`);
|
|
207
|
+
}
|
|
208
|
+
const before = snapshotRules(raw);
|
|
209
|
+
for (const entry of raw) {
|
|
210
|
+
if (typeof entry === "object" &&
|
|
211
|
+
entry !== null &&
|
|
212
|
+
!Array.isArray(entry) &&
|
|
213
|
+
entry.label === label) {
|
|
214
|
+
return [false, `already-ignored (label=${label})`];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
raw.push({ label });
|
|
218
|
+
atomicWriteProjectDefinition(path, data);
|
|
219
|
+
const after = snapshotRules(raw);
|
|
220
|
+
recordSubscriptionChange(projectRoot, {
|
|
221
|
+
op: "ignore-label",
|
|
222
|
+
label,
|
|
223
|
+
before,
|
|
224
|
+
after,
|
|
225
|
+
actor: null,
|
|
226
|
+
});
|
|
227
|
+
return [true, `added ignore (label=${label})`];
|
|
217
228
|
});
|
|
218
|
-
return [true, `added ignore (label=${label})`];
|
|
219
229
|
}
|
|
220
230
|
//# sourceMappingURL=mutations-core.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
3
4
|
import { readPlanPolicy } from "../../policy/plan-extensions.js";
|
|
4
|
-
import { DEFAULT_TRIAGE_SCOPE
|
|
5
|
+
import { DEFAULT_TRIAGE_SCOPE } from "./constants.js";
|
|
5
6
|
export function projectDefinitionPath(projectRoot) {
|
|
6
|
-
return
|
|
7
|
+
return resolveProjectDefinitionPath(resolve(projectRoot));
|
|
7
8
|
}
|
|
8
9
|
export function loadProjectDefinition(projectRoot) {
|
|
9
10
|
const path = projectDefinitionPath(projectRoot);
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
4
4
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
5
|
-
|
|
6
|
-
function projectDefinitionPath(projectRoot) {
|
|
7
|
-
return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
|
|
8
|
-
}
|
|
5
|
+
import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
|
|
9
6
|
function loadForMutation(projectRoot) {
|
|
10
|
-
const path =
|
|
7
|
+
const path = resolveProjectDefinitionPath(resolve(projectRoot));
|
|
11
8
|
if (!existsSync(path)) {
|
|
12
9
|
throw new Error(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / task triage:bootstrap to scaffold one first.`);
|
|
13
10
|
}
|
|
@@ -49,32 +46,36 @@ export function addIgnore(projectRoot, options) {
|
|
|
49
46
|
if (typeof value !== "string" || !value.trim()) {
|
|
50
47
|
throw new Error(`${key} must be a non-empty string; got ${JSON.stringify(value)}`);
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
planRec[PLAN_POLICY_KEY]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
49
|
+
// Serialise the read-modify-write under the shared PROJECT-DEFINITION
|
|
50
|
+
// mutation lock so concurrent mutators cannot lose an update (#1260).
|
|
51
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
52
|
+
const [data, path] = loadForMutation(projectRoot);
|
|
53
|
+
const plan = data.plan;
|
|
54
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
55
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
|
|
56
|
+
}
|
|
57
|
+
const planRec = plan;
|
|
58
|
+
migrateLegacyPolicyKey(planRec);
|
|
59
|
+
let policy = planRec[PLAN_POLICY_KEY];
|
|
60
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
61
|
+
policy = {};
|
|
62
|
+
planRec[PLAN_POLICY_KEY] = policy;
|
|
63
|
+
}
|
|
64
|
+
const policyRec = policy;
|
|
65
|
+
const raw = Array.isArray(policyRec.triageScopeIgnores)
|
|
66
|
+
? policyRec.triageScopeIgnores
|
|
67
|
+
: [];
|
|
68
|
+
policyRec.triageScopeIgnores = raw;
|
|
69
|
+
for (const entry of raw) {
|
|
70
|
+
if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
|
|
71
|
+
if (entry[key] === value) {
|
|
72
|
+
return { changed: false, message: `already-ignored (${key}=${value})` };
|
|
73
|
+
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
raw.push({ [key]: value });
|
|
77
|
+
atomicWrite(path, data);
|
|
78
|
+
return { changed: true, message: `added ignore (${key}=${value})` };
|
|
79
|
+
});
|
|
79
80
|
}
|
|
80
81
|
//# sourceMappingURL=add-ignore.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
3
4
|
import { readPlanPolicy } from "../../policy/plan-extensions.js";
|
|
4
|
-
const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
|
|
5
5
|
const DEFAULT_TRIAGE_SCOPE = [{ rule: "all-open" }];
|
|
6
6
|
function loadProjectDefinition(projectRoot) {
|
|
7
|
-
const path =
|
|
7
|
+
const path = resolveProjectDefinitionPath(resolve(projectRoot));
|
|
8
8
|
if (!existsSync(path))
|
|
9
9
|
return null;
|
|
10
10
|
try {
|
|
@@ -3,6 +3,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlink
|
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
5
5
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
6
|
+
import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
|
|
6
7
|
export const SUBSCRIPTION_HISTORY_REL_PATH = "vbrief/.eval/subscription-history.jsonl";
|
|
7
8
|
export const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-change.v1";
|
|
8
9
|
export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
|
|
@@ -246,57 +247,62 @@ function mutate(projectRoot, options) {
|
|
|
246
247
|
if (chosen.length !== 1) {
|
|
247
248
|
throw new Error(`${options.op}() requires exactly one of --label / --milestone / --issue; got ${JSON.stringify(chosen)}`);
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (typeof existingPolicy !== "object" ||
|
|
257
|
-
existingPolicy === null ||
|
|
258
|
-
Array.isArray(existingPolicy)) {
|
|
259
|
-
if (existingPolicy === undefined) {
|
|
260
|
-
plan[PLAN_POLICY_KEY] = {};
|
|
250
|
+
// Serialise the read-modify-write + subscription-history append under the
|
|
251
|
+
// shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
|
|
252
|
+
// an update or emit out-of-order audit rows (#1260).
|
|
253
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
254
|
+
const [data, path] = loadProjectDefinitionForMutation(projectRoot);
|
|
255
|
+
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
256
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
|
|
261
257
|
}
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
const plan = data.plan;
|
|
259
|
+
migrateLegacyPolicyKey(plan);
|
|
260
|
+
const existingPolicy = plan[PLAN_POLICY_KEY];
|
|
261
|
+
if (typeof existingPolicy !== "object" ||
|
|
262
|
+
existingPolicy === null ||
|
|
263
|
+
Array.isArray(existingPolicy)) {
|
|
264
|
+
if (existingPolicy === undefined) {
|
|
265
|
+
plan[PLAN_POLICY_KEY] = {};
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
|
|
269
|
+
}
|
|
264
270
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
const policy = plan[PLAN_POLICY_KEY];
|
|
272
|
+
if (!Array.isArray(policy.triageScope)) {
|
|
273
|
+
if (policy.triageScope === undefined) {
|
|
274
|
+
policy.triageScope = [];
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const rules = policy.triageScope;
|
|
281
|
+
const before = snapshotRules(rules);
|
|
282
|
+
let changed;
|
|
283
|
+
let message;
|
|
284
|
+
if (options.op === "subscribe") {
|
|
285
|
+
[changed, message] = applySubscribe(rules, options.label, options.milestone, options.issue, options.issueNote ?? "added via task triage:subscribe");
|
|
270
286
|
}
|
|
271
287
|
else {
|
|
272
|
-
|
|
288
|
+
[changed, message] = applyUnsubscribe(rules, options.label, options.milestone, options.issue);
|
|
273
289
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const after = snapshotRules(rules);
|
|
290
|
-
recordSubscriptionChange(projectRoot, {
|
|
291
|
-
op: options.op,
|
|
292
|
-
label: options.label,
|
|
293
|
-
milestone: options.milestone,
|
|
294
|
-
issue: options.issue,
|
|
295
|
-
before,
|
|
296
|
-
after,
|
|
297
|
-
actor: options.actor,
|
|
290
|
+
if (!changed) {
|
|
291
|
+
return [false, message];
|
|
292
|
+
}
|
|
293
|
+
atomicWriteProjectDefinition(path, data);
|
|
294
|
+
const after = snapshotRules(rules);
|
|
295
|
+
recordSubscriptionChange(projectRoot, {
|
|
296
|
+
op: options.op,
|
|
297
|
+
label: options.label,
|
|
298
|
+
milestone: options.milestone,
|
|
299
|
+
issue: options.issue,
|
|
300
|
+
before,
|
|
301
|
+
after,
|
|
302
|
+
actor: options.actor,
|
|
303
|
+
});
|
|
304
|
+
return [true, message];
|
|
298
305
|
});
|
|
299
|
-
return [true, message];
|
|
300
306
|
}
|
|
301
307
|
export function subscribe(projectRoot, options = {}) {
|
|
302
308
|
return mutate(projectRoot, { op: "subscribe", ...options });
|