@deftai/directive-core 0.107.0 → 0.109.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/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
* gitignored / package-manager-invisible). Only the global, project-local,
|
|
17
17
|
* ephemeral, and upgrade command forms vary by package manager.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Detection from an injected fact-set remains pure. The project resolver is the
|
|
20
|
+
* single filesystem boundary used by consumer gates so package.json and lockfile
|
|
21
|
+
* precedence cannot drift across callers (#3610).
|
|
21
22
|
*/
|
|
23
|
+
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { join, resolve } from "node:path";
|
|
22
25
|
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
23
26
|
export const DEFAULT_PACKAGE_MANAGER = "npm";
|
|
24
27
|
/** Canonical published engine package name. */
|
|
25
28
|
export const ENGINE_PACKAGE = "@deftai/directive";
|
|
29
|
+
const EXPLICIT_PACKAGE_MANAGER_RE = /^(npm|pnpm)(?:@[0-9A-Za-z][0-9A-Za-z._+/=-]*)?$/i;
|
|
26
30
|
function normalizePackageManager(value) {
|
|
27
31
|
const v = value.trim().toLowerCase();
|
|
28
32
|
if (v.startsWith("pnpm"))
|
|
@@ -31,24 +35,221 @@ function normalizePackageManager(value) {
|
|
|
31
35
|
return "npm";
|
|
32
36
|
return null;
|
|
33
37
|
}
|
|
38
|
+
function normalizeExplicitPackageManager(value) {
|
|
39
|
+
const match = EXPLICIT_PACKAGE_MANAGER_RE.exec(value.trim());
|
|
40
|
+
const name = match?.[1]?.toLowerCase();
|
|
41
|
+
return name === "npm" || name === "pnpm" ? name : null;
|
|
42
|
+
}
|
|
43
|
+
function unsupportedPackageManager(value, source) {
|
|
44
|
+
if (source === "env-override") {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
source,
|
|
48
|
+
message: "Unsupported DEFT_PACKAGE_MANAGER value; supported managers are npm and pnpm.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const safeValue = sanitizeSingleLineDiagnostic(JSON.stringify(value));
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
source,
|
|
55
|
+
message: `Unsupported package manager ${safeValue} from package.json#packageManager; supported managers are npm and pnpm.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Human-readable, non-executable label for a resolution source. */
|
|
59
|
+
export function packageManagerSourceLabel(source) {
|
|
60
|
+
switch (source) {
|
|
61
|
+
case "env-override":
|
|
62
|
+
return "DEFT_PACKAGE_MANAGER override";
|
|
63
|
+
case "package-manager-field":
|
|
64
|
+
return "packageManager field";
|
|
65
|
+
case "pnpm-lock":
|
|
66
|
+
return "pnpm-lock.yaml";
|
|
67
|
+
case "user-agent":
|
|
68
|
+
return "npm_config_user_agent";
|
|
69
|
+
case "default":
|
|
70
|
+
return "default";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Strict package-manager resolution for execution boundaries.
|
|
75
|
+
*
|
|
76
|
+
* Explicit override/declaration values fail closed when unsupported. The
|
|
77
|
+
* returned manager is a closed union and callers map it to fixed argv; raw
|
|
78
|
+
* package.json or environment text is never executable (#2761/#2765/#3610).
|
|
79
|
+
*/
|
|
80
|
+
function packageManagerConflictResolution() {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
source: "env-override",
|
|
84
|
+
message: "DEFT_PACKAGE_MANAGER conflicts with package.json#packageManager; the declaration is authoritative.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveExplicitOverride(override) {
|
|
88
|
+
if (override.trim() === "")
|
|
89
|
+
return null;
|
|
90
|
+
const packageManager = normalizeExplicitPackageManager(override);
|
|
91
|
+
if (packageManager === null)
|
|
92
|
+
return unsupportedPackageManager(override, "env-override");
|
|
93
|
+
return { ok: true, packageManager, source: "env-override" };
|
|
94
|
+
}
|
|
95
|
+
export function resolvePackageManager(input = {}) {
|
|
96
|
+
const env = input.env ?? {};
|
|
97
|
+
const override = env.DEFT_PACKAGE_MANAGER;
|
|
98
|
+
const overrideText = typeof override === "string" ? override : "";
|
|
99
|
+
const field = input.packageManagerField;
|
|
100
|
+
if (field != null && field.trim() !== "") {
|
|
101
|
+
const declared = normalizeExplicitPackageManager(field);
|
|
102
|
+
if (declared === null)
|
|
103
|
+
return unsupportedPackageManager(field, "package-manager-field");
|
|
104
|
+
if (overrideText.trim() !== "") {
|
|
105
|
+
const overridden = normalizeExplicitPackageManager(overrideText);
|
|
106
|
+
if (overridden !== null && overridden !== declared) {
|
|
107
|
+
return packageManagerConflictResolution();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, packageManager: declared, source: "package-manager-field" };
|
|
111
|
+
}
|
|
112
|
+
const overrideResolution = resolveExplicitOverride(overrideText);
|
|
113
|
+
if (overrideResolution !== null)
|
|
114
|
+
return overrideResolution;
|
|
115
|
+
if (input.pnpmLockPresent) {
|
|
116
|
+
return { ok: true, packageManager: "pnpm", source: "pnpm-lock" };
|
|
117
|
+
}
|
|
118
|
+
const userAgent = env.npm_config_user_agent;
|
|
119
|
+
if (typeof userAgent === "string" && userAgent.trim() !== "") {
|
|
120
|
+
const packageManager = normalizePackageManager(userAgent);
|
|
121
|
+
if (packageManager !== null) {
|
|
122
|
+
return { ok: true, packageManager, source: "user-agent" };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, packageManager: DEFAULT_PACKAGE_MANAGER, source: "default" };
|
|
126
|
+
}
|
|
127
|
+
function defaultReadText(path) {
|
|
128
|
+
try {
|
|
129
|
+
return readFileSync(path, "utf8");
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error.code === "ENOENT")
|
|
133
|
+
return null;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function defaultIsFile(path) {
|
|
138
|
+
try {
|
|
139
|
+
return statSync(path).isFile();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code === "ENOENT")
|
|
143
|
+
return false;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const UNSAFE_DIAGNOSTIC_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
148
|
+
/** Render untrusted diagnostic text as one bounded terminal-safe line. */
|
|
149
|
+
export function sanitizeSingleLineDiagnostic(text) {
|
|
150
|
+
const printable = [...text]
|
|
151
|
+
.map((character) => (UNSAFE_DIAGNOSTIC_CHARACTER.test(character) ? " " : character))
|
|
152
|
+
.join("")
|
|
153
|
+
.replace(/\s+/g, " ")
|
|
154
|
+
.trim();
|
|
155
|
+
const codePoints = [...printable];
|
|
156
|
+
return codePoints.length > 240 ? `${codePoints.slice(0, 237).join("")}...` : printable;
|
|
157
|
+
}
|
|
158
|
+
function singleLineErrorDetail(error) {
|
|
159
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
160
|
+
return sanitizeSingleLineDiagnostic(raw) || "unknown error";
|
|
161
|
+
}
|
|
162
|
+
/** Resolve a consumer project's package manager from package.json and lockfile facts. */
|
|
163
|
+
export function resolveProjectPackageManager(options = {}) {
|
|
164
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
165
|
+
const env = options.env ?? process.env;
|
|
166
|
+
const readText = options.readText ?? defaultReadText;
|
|
167
|
+
const isFile = options.isFile ?? defaultIsFile;
|
|
168
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
169
|
+
let packageManagerField = null;
|
|
170
|
+
let packageJsonText;
|
|
171
|
+
try {
|
|
172
|
+
packageJsonText = readText(packageJsonPath);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const detail = singleLineErrorDetail(error);
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
source: "package-json",
|
|
179
|
+
message: `Could not read package.json for package-manager selection: ${detail}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (packageJsonText !== null) {
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(packageJsonText);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const detail = singleLineErrorDetail(error);
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
source: "package-json",
|
|
192
|
+
message: `Could not parse package.json for package-manager selection: ${detail}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
source: "package-json",
|
|
199
|
+
message: "Could not parse package.json for package-manager selection: root must be an object.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const packageRecord = parsed;
|
|
203
|
+
if (Object.hasOwn(packageRecord, "packageManager")) {
|
|
204
|
+
const field = packageRecord.packageManager;
|
|
205
|
+
if (typeof field !== "string" || field.trim() === "") {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
source: "package-manager-field",
|
|
209
|
+
message: "package.json#packageManager must be a non-empty string naming npm or pnpm.",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
packageManagerField = field;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (packageManagerField !== null) {
|
|
216
|
+
return resolvePackageManager({ env, packageManagerField });
|
|
217
|
+
}
|
|
218
|
+
let pnpmLockPresent;
|
|
219
|
+
try {
|
|
220
|
+
pnpmLockPresent = isFile(join(projectRoot, "pnpm-lock.yaml"));
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const detail = singleLineErrorDetail(error);
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
source: "project-filesystem",
|
|
227
|
+
message: `Could not inspect pnpm-lock.yaml for package-manager selection: ${detail}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return resolvePackageManager({
|
|
231
|
+
env,
|
|
232
|
+
pnpmLockPresent,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
34
235
|
/**
|
|
35
236
|
* Detect the active package manager. Precedence (first match wins):
|
|
36
|
-
* 1. `
|
|
37
|
-
* 2. `
|
|
237
|
+
* 1. `packageManager` field / Corepack shim (authoritative when present)
|
|
238
|
+
* 2. `DEFT_PACKAGE_MANAGER` env override
|
|
38
239
|
* 3. `pnpm-lock.yaml` present
|
|
39
240
|
* 4. `npm_config_user_agent` (set by the manager that spawned the process)
|
|
40
241
|
* 5. default: npm
|
|
41
242
|
*/
|
|
42
243
|
export function detectPackageManager(input = {}) {
|
|
43
244
|
const env = input.env ?? {};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const pm = normalizePackageManager(override);
|
|
245
|
+
if (input.packageManagerField != null && input.packageManagerField.trim() !== "") {
|
|
246
|
+
const pm = normalizePackageManager(input.packageManagerField);
|
|
47
247
|
if (pm)
|
|
48
248
|
return pm;
|
|
49
249
|
}
|
|
50
|
-
|
|
51
|
-
|
|
250
|
+
const override = env.DEFT_PACKAGE_MANAGER;
|
|
251
|
+
if (typeof override === "string" && override.trim() !== "") {
|
|
252
|
+
const pm = normalizePackageManager(override);
|
|
52
253
|
if (pm)
|
|
53
254
|
return pm;
|
|
54
255
|
}
|
|
@@ -14,6 +14,13 @@ export declare const MONITORING_TIER_1 = 1;
|
|
|
14
14
|
export declare const MONITORING_TIER_2 = 2;
|
|
15
15
|
export declare const MONITORING_TIER_3 = 3;
|
|
16
16
|
export declare const DEFAULT_STALE_MINUTES = 30;
|
|
17
|
+
/** `kind:` value marking an advisory pass-open mark rather than an ownership lease (#3607). */
|
|
18
|
+
export declare const PASS_MARKER_KIND = "pass";
|
|
19
|
+
/**
|
|
20
|
+
* Advisory pass marks expire on read (#3607). Passes run in minutes, so an hour
|
|
21
|
+
* bounds an abandoned mark without needing a heartbeat.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_PASS_STALE_MINUTES = 60;
|
|
17
24
|
export declare const REVIEW_MONITOR_HELP: string;
|
|
18
25
|
export declare const REGISTER_HELP: string;
|
|
19
26
|
export declare const RELEASE_HELP: string;
|
|
@@ -14,6 +14,13 @@ export const MONITORING_TIER_1 = 1;
|
|
|
14
14
|
export const MONITORING_TIER_2 = 2;
|
|
15
15
|
export const MONITORING_TIER_3 = 3;
|
|
16
16
|
export const DEFAULT_STALE_MINUTES = 30;
|
|
17
|
+
/** `kind:` value marking an advisory pass-open mark rather than an ownership lease (#3607). */
|
|
18
|
+
export const PASS_MARKER_KIND = "pass";
|
|
19
|
+
/**
|
|
20
|
+
* Advisory pass marks expire on read (#3607). Passes run in minutes, so an hour
|
|
21
|
+
* bounds an abandoned mark without needing a heartbeat.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_PASS_STALE_MINUTES = 60;
|
|
17
24
|
export const REVIEW_MONITOR_HELP = "usage: task verify:review-monitor -- --pr <N> [options]\n" +
|
|
18
25
|
"\n" +
|
|
19
26
|
"Fail-closed gate: when Tier 1 (sub-agent primitive available), require an\n" +
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type GhRestSeams } from "../scm/gh-rest.js";
|
|
2
2
|
import { type IssueComment } from "../umbrella-current-shape/index.js";
|
|
3
|
-
import { type ReviewOwnerComment } from "./lease-comment.js";
|
|
3
|
+
import { type PassOpenComment, type PassOpenMarker, type ReviewOwnerComment } from "./lease-comment.js";
|
|
4
4
|
export type { IssueComment };
|
|
5
5
|
export interface ReviewOwnerGithubSeams extends GhRestSeams {
|
|
6
6
|
readonly fetchComments?: (repo: string, pr: number) => IssueComment[] | {
|
|
@@ -42,4 +42,101 @@ export declare function deleteReviewOwnerComment(repo: string, commentId: number
|
|
|
42
42
|
error: string;
|
|
43
43
|
};
|
|
44
44
|
export declare function resolveGitHubLogin(seams?: ReviewOwnerGithubSeams): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Author trust boundary for `<!-- deft:review-owner -->` comments (#3607 / #2307).
|
|
47
|
+
*
|
|
48
|
+
* Ownership leases gate. `verify:review-monitor` and `verify:l4-owner` exit 0 on a live
|
|
49
|
+
* lease, so a forged lease defeats a fail-closed gate. `listReviewOwnerComments` above
|
|
50
|
+
* therefore admits only maintainer associations -- OWNER, MEMBER, COLLABORATOR. That
|
|
51
|
+
* filter is the single author-association check in the lease read path and it lives in
|
|
52
|
+
* `isMaintainerAuthored` / `MAINTAINER_ASSOCIATIONS`
|
|
53
|
+
* (`packages/core/src/umbrella-current-shape/index.ts`), which is why a search for
|
|
54
|
+
* `author_association` in this directory finds nothing.
|
|
55
|
+
*
|
|
56
|
+
* Pass marks do not gate. They tell an arriving agent that a pass is open and never
|
|
57
|
+
* block a write, so a forged mark costs at most an unnecessary courtesy. Pass marks are
|
|
58
|
+
* therefore read from every author association, including CONTRIBUTOR, which is what
|
|
59
|
+
* #3607 requires ("visible regardless of the author's trust association"). The field
|
|
60
|
+
* instance is real: comment 5429316778 on PR #3775 carries this marker and is
|
|
61
|
+
* CONTRIBUTOR-authored, so the lease filter hides it while this reader sees it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function listPassMarkerComments(repo: string, issue: number, seams?: ReviewOwnerGithubSeams): PassOpenComment[] | {
|
|
64
|
+
error: string;
|
|
65
|
+
};
|
|
66
|
+
export interface ActivePassMarker {
|
|
67
|
+
readonly commentId: number;
|
|
68
|
+
readonly marker: PassOpenMarker;
|
|
69
|
+
}
|
|
70
|
+
/** Read the open mark on an issue thread; expired and cleared marks are ignored (#3607). */
|
|
71
|
+
export declare function fetchActivePassMarker(repo: string, issue: number, options?: {
|
|
72
|
+
now?: Date;
|
|
73
|
+
seams?: ReviewOwnerGithubSeams;
|
|
74
|
+
}): ActivePassMarker | null | {
|
|
75
|
+
error: string;
|
|
76
|
+
};
|
|
77
|
+
export interface OpenPassMarkerInput {
|
|
78
|
+
readonly repo: string;
|
|
79
|
+
readonly issue: number;
|
|
80
|
+
readonly owner: string;
|
|
81
|
+
readonly passKind: string;
|
|
82
|
+
readonly agentId?: string | null;
|
|
83
|
+
readonly ceiling?: string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Comment id returned by a previous open, to refresh that same mark in place.
|
|
86
|
+
* Only the pass that created a comment ever writes it again, which is what keeps
|
|
87
|
+
* two concurrent passes from overwriting one another (#3607).
|
|
88
|
+
*/
|
|
89
|
+
readonly commentId?: number | null;
|
|
90
|
+
readonly startedAt?: Date;
|
|
91
|
+
readonly staleMinutes?: number;
|
|
92
|
+
readonly seams?: ReviewOwnerGithubSeams;
|
|
93
|
+
}
|
|
94
|
+
export type OpenPassMarkerResult = {
|
|
95
|
+
readonly status: "opened" | "renewed" | "observed";
|
|
96
|
+
readonly commentId: number;
|
|
97
|
+
readonly marker: PassOpenMarker;
|
|
98
|
+
} | {
|
|
99
|
+
readonly error: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Mark a pass open on an issue thread (#3607).
|
|
103
|
+
*
|
|
104
|
+
* A marker comment belongs to the one pass that created it. An open never edits a
|
|
105
|
+
* comment it did not create -- it creates its own, or reports the mark already there --
|
|
106
|
+
* so two concurrent passes can never overwrite each other's metadata, including two
|
|
107
|
+
* agents sharing one GitHub login. Refreshing an existing mark requires the `commentId`
|
|
108
|
+
* that open handed back, and the mark on that comment is re-read and checked against the
|
|
109
|
+
* caller before the write: a `commentId` taken from an `observed` result names another
|
|
110
|
+
* pass, so it is refused and the caller opens its own mark instead of overwriting.
|
|
111
|
+
*
|
|
112
|
+
* `observed` means a mark is already open on the thread. The caller is informed and MAY
|
|
113
|
+
* still write; nothing is held. Concurrent creates resolve oldest-comment-id wins, and
|
|
114
|
+
* the loser removes its own duplicate.
|
|
115
|
+
*/
|
|
116
|
+
export declare function openPassMarker(input: OpenPassMarkerInput): OpenPassMarkerResult;
|
|
117
|
+
export interface ClosePassMarkerInput {
|
|
118
|
+
readonly repo: string;
|
|
119
|
+
readonly issue: number;
|
|
120
|
+
readonly owner: string;
|
|
121
|
+
/** Comment id from open. Without it, the oldest open mark this owner authored. */
|
|
122
|
+
readonly commentId?: number | null;
|
|
123
|
+
readonly endedAt?: Date;
|
|
124
|
+
readonly seams?: ReviewOwnerGithubSeams;
|
|
125
|
+
}
|
|
126
|
+
export type ClosePassMarkerResult = {
|
|
127
|
+
readonly status: "cleared" | "not-open" | "held-by-other";
|
|
128
|
+
readonly commentId: number | null;
|
|
129
|
+
} | {
|
|
130
|
+
readonly error: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Clear the mark at synthesis (#3607).
|
|
134
|
+
*
|
|
135
|
+
* `ended_at` is rendered over the marker as just read from the thread, never over the
|
|
136
|
+
* snapshot the caller opened with, so closing does not resurrect stale pass metadata.
|
|
137
|
+
* A mark the thread does not prove is the caller's own -- another author's, another
|
|
138
|
+
* owner's, or one whose author GitHub did not report -- is reported back, never
|
|
139
|
+
* overwritten.
|
|
140
|
+
*/
|
|
141
|
+
export declare function closePassMarker(input: ClosePassMarkerInput): ClosePassMarkerResult;
|
|
45
142
|
//# sourceMappingURL=github-lease.d.ts.map
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveBinaryForArgv } from "../scm/call-shape.js";
|
|
3
3
|
import { DEFAULT_TIMEOUT_S, restDeleteComment, restGetUser, restPostComment, restUpdateComment, } from "../scm/gh-rest.js";
|
|
4
4
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
5
5
|
import { isMaintainerAuthored, parseCommentsFromGhStdout, } from "../umbrella-current-shape/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DEFAULT_PASS_STALE_MINUTES, PASS_MARKER_KIND } from "./constants.js";
|
|
7
|
+
import { computeExpiresAt, findActivePassComment, mapCommentEntry, mapPassCommentEntry, renderPassOpenComment, } from "./lease-comment.js";
|
|
7
8
|
function defaultFetchComments(repo, pr, seams) {
|
|
8
|
-
const binary = resolveBinary(seams.whichFn);
|
|
9
9
|
const path = `repos/${repo}/issues/${pr}/comments?per_page=100`;
|
|
10
|
-
const
|
|
10
|
+
const apiArgs = ["--paginate", path];
|
|
11
|
+
const binary = resolveBinaryForArgv("api", apiArgs, seams.whichFn);
|
|
12
|
+
const proc = spawnSync(binary, ["api", ...apiArgs], {
|
|
11
13
|
encoding: "utf8",
|
|
12
14
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
13
15
|
timeout: DEFAULT_TIMEOUT_S * 1000,
|
|
@@ -103,4 +105,184 @@ export function resolveGitHubLogin(seams = {}) {
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Author trust boundary for `<!-- deft:review-owner -->` comments (#3607 / #2307).
|
|
110
|
+
*
|
|
111
|
+
* Ownership leases gate. `verify:review-monitor` and `verify:l4-owner` exit 0 on a live
|
|
112
|
+
* lease, so a forged lease defeats a fail-closed gate. `listReviewOwnerComments` above
|
|
113
|
+
* therefore admits only maintainer associations -- OWNER, MEMBER, COLLABORATOR. That
|
|
114
|
+
* filter is the single author-association check in the lease read path and it lives in
|
|
115
|
+
* `isMaintainerAuthored` / `MAINTAINER_ASSOCIATIONS`
|
|
116
|
+
* (`packages/core/src/umbrella-current-shape/index.ts`), which is why a search for
|
|
117
|
+
* `author_association` in this directory finds nothing.
|
|
118
|
+
*
|
|
119
|
+
* Pass marks do not gate. They tell an arriving agent that a pass is open and never
|
|
120
|
+
* block a write, so a forged mark costs at most an unnecessary courtesy. Pass marks are
|
|
121
|
+
* therefore read from every author association, including CONTRIBUTOR, which is what
|
|
122
|
+
* #3607 requires ("visible regardless of the author's trust association"). The field
|
|
123
|
+
* instance is real: comment 5429316778 on PR #3775 carries this marker and is
|
|
124
|
+
* CONTRIBUTOR-authored, so the lease filter hides it while this reader sees it.
|
|
125
|
+
*/
|
|
126
|
+
export function listPassMarkerComments(repo, issue, seams = {}) {
|
|
127
|
+
const fetcher = seams.fetchComments ?? ((r, n) => defaultFetchComments(r, n, seams));
|
|
128
|
+
const fetched = fetcher(repo, issue);
|
|
129
|
+
if (!Array.isArray(fetched)) {
|
|
130
|
+
return fetched;
|
|
131
|
+
}
|
|
132
|
+
const comments = [];
|
|
133
|
+
for (const entry of fetched) {
|
|
134
|
+
const mapped = mapPassCommentEntry({
|
|
135
|
+
id: entry.id,
|
|
136
|
+
body: entry.body,
|
|
137
|
+
created_at: entry.updatedAt,
|
|
138
|
+
author_login: entry.authorLogin,
|
|
139
|
+
});
|
|
140
|
+
if (mapped !== null) {
|
|
141
|
+
comments.push(mapped);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return comments;
|
|
145
|
+
}
|
|
146
|
+
/** Read the open mark on an issue thread; expired and cleared marks are ignored (#3607). */
|
|
147
|
+
export function fetchActivePassMarker(repo, issue, options = {}) {
|
|
148
|
+
const listed = listPassMarkerComments(repo, issue, options.seams ?? {});
|
|
149
|
+
if (!Array.isArray(listed)) {
|
|
150
|
+
return { error: listed.error };
|
|
151
|
+
}
|
|
152
|
+
const active = findActivePassComment(listed, { now: options.now });
|
|
153
|
+
if (active === null || active.marker === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return { commentId: active.id, marker: active.marker };
|
|
157
|
+
}
|
|
158
|
+
function buildPassMarker(input, startedAt) {
|
|
159
|
+
return {
|
|
160
|
+
kind: PASS_MARKER_KIND,
|
|
161
|
+
pass_kind: input.passKind,
|
|
162
|
+
owner: input.owner,
|
|
163
|
+
agent_id: input.agentId ?? null,
|
|
164
|
+
ceiling: input.ceiling ?? null,
|
|
165
|
+
started_at: startedAt.toISOString(),
|
|
166
|
+
expires_at: computeExpiresAt(startedAt, input.staleMinutes ?? DEFAULT_PASS_STALE_MINUTES),
|
|
167
|
+
ended_at: null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function resolvePassCreateRace(input, createdId, marker, now, seams) {
|
|
171
|
+
const relisted = listPassMarkerComments(input.repo, input.issue, seams);
|
|
172
|
+
if (!Array.isArray(relisted)) {
|
|
173
|
+
return { error: relisted.error };
|
|
174
|
+
}
|
|
175
|
+
const winner = findActivePassComment(relisted, { now });
|
|
176
|
+
if (winner === null || winner.marker === null || winner.id === createdId) {
|
|
177
|
+
return { status: "opened", commentId: createdId, marker };
|
|
178
|
+
}
|
|
179
|
+
const deleted = deleteReviewOwnerComment(input.repo, createdId, seams);
|
|
180
|
+
if ("error" in deleted) {
|
|
181
|
+
return { error: deleted.error };
|
|
182
|
+
}
|
|
183
|
+
return { status: "observed", commentId: winner.id, marker: winner.marker };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Proof, read from the thread, that a mark is the caller's own to write (#3607).
|
|
187
|
+
*
|
|
188
|
+
* Both the comment's GitHub author and the marker's `owner` must be the caller. An
|
|
189
|
+
* author GitHub did not report is refused rather than assumed: an unreported author is
|
|
190
|
+
* exactly the case where a marker body claiming the caller as `owner` is unverifiable,
|
|
191
|
+
* and PATCHing another user's comment is a 403 for a non-maintainer.
|
|
192
|
+
*/
|
|
193
|
+
function isOwnOpenPassComment(comment, owner) {
|
|
194
|
+
const mark = comment.marker;
|
|
195
|
+
return (mark !== null &&
|
|
196
|
+
mark.ended_at === null &&
|
|
197
|
+
comment.authorLogin.length > 0 &&
|
|
198
|
+
comment.authorLogin === owner &&
|
|
199
|
+
mark.owner === owner);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A refresh must name the pass it refreshes (#3607). Owner login, agent id and pass kind
|
|
203
|
+
* identify a pass; the ceiling and the expiry window are what a refresh moves. Two runs
|
|
204
|
+
* agreeing on all three are one pass identity by construction, so a refresh then moves
|
|
205
|
+
* only that pass's own window.
|
|
206
|
+
*/
|
|
207
|
+
function refreshesSamePass(marker, input) {
|
|
208
|
+
return marker.pass_kind === input.passKind && marker.agent_id === (input.agentId ?? null);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Mark a pass open on an issue thread (#3607).
|
|
212
|
+
*
|
|
213
|
+
* A marker comment belongs to the one pass that created it. An open never edits a
|
|
214
|
+
* comment it did not create -- it creates its own, or reports the mark already there --
|
|
215
|
+
* so two concurrent passes can never overwrite each other's metadata, including two
|
|
216
|
+
* agents sharing one GitHub login. Refreshing an existing mark requires the `commentId`
|
|
217
|
+
* that open handed back, and the mark on that comment is re-read and checked against the
|
|
218
|
+
* caller before the write: a `commentId` taken from an `observed` result names another
|
|
219
|
+
* pass, so it is refused and the caller opens its own mark instead of overwriting.
|
|
220
|
+
*
|
|
221
|
+
* `observed` means a mark is already open on the thread. The caller is informed and MAY
|
|
222
|
+
* still write; nothing is held. Concurrent creates resolve oldest-comment-id wins, and
|
|
223
|
+
* the loser removes its own duplicate.
|
|
224
|
+
*/
|
|
225
|
+
export function openPassMarker(input) {
|
|
226
|
+
const seams = input.seams ?? {};
|
|
227
|
+
const startedAt = input.startedAt ?? new Date();
|
|
228
|
+
const marker = buildPassMarker(input, startedAt);
|
|
229
|
+
const body = renderPassOpenComment(marker);
|
|
230
|
+
const listed = listPassMarkerComments(input.repo, input.issue, seams);
|
|
231
|
+
if (!Array.isArray(listed)) {
|
|
232
|
+
return { error: listed.error };
|
|
233
|
+
}
|
|
234
|
+
if (input.commentId !== undefined && input.commentId !== null) {
|
|
235
|
+
const target = listed.find((comment) => comment.id === input.commentId);
|
|
236
|
+
if (target !== undefined &&
|
|
237
|
+
isOwnOpenPassComment(target, input.owner) &&
|
|
238
|
+
refreshesSamePass(target.marker, input)) {
|
|
239
|
+
const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
|
|
240
|
+
if ("error" in updated) {
|
|
241
|
+
return { error: updated.error };
|
|
242
|
+
}
|
|
243
|
+
return { status: "renewed", commentId: target.id, marker };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const active = findActivePassComment(listed, { now: startedAt });
|
|
247
|
+
if (active !== null && active.marker !== null) {
|
|
248
|
+
return { status: "observed", commentId: active.id, marker: active.marker };
|
|
249
|
+
}
|
|
250
|
+
const created = createReviewOwnerComment(input.repo, input.issue, body, seams);
|
|
251
|
+
if ("error" in created) {
|
|
252
|
+
return { error: created.error };
|
|
253
|
+
}
|
|
254
|
+
return resolvePassCreateRace(input, created.id, marker, startedAt, seams);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Clear the mark at synthesis (#3607).
|
|
258
|
+
*
|
|
259
|
+
* `ended_at` is rendered over the marker as just read from the thread, never over the
|
|
260
|
+
* snapshot the caller opened with, so closing does not resurrect stale pass metadata.
|
|
261
|
+
* A mark the thread does not prove is the caller's own -- another author's, another
|
|
262
|
+
* owner's, or one whose author GitHub did not report -- is reported back, never
|
|
263
|
+
* overwritten.
|
|
264
|
+
*/
|
|
265
|
+
export function closePassMarker(input) {
|
|
266
|
+
const seams = input.seams ?? {};
|
|
267
|
+
const endedAt = input.endedAt ?? new Date();
|
|
268
|
+
const listed = listPassMarkerComments(input.repo, input.issue, seams);
|
|
269
|
+
if (!Array.isArray(listed)) {
|
|
270
|
+
return { error: listed.error };
|
|
271
|
+
}
|
|
272
|
+
const target = input.commentId !== undefined && input.commentId !== null
|
|
273
|
+
? (listed.find((comment) => comment.id === input.commentId) ?? null)
|
|
274
|
+
: findActivePassComment(listed, { now: endedAt });
|
|
275
|
+
if (target === null || target.marker === null || target.marker.ended_at !== null) {
|
|
276
|
+
return { status: "not-open", commentId: null };
|
|
277
|
+
}
|
|
278
|
+
if (!isOwnOpenPassComment(target, input.owner)) {
|
|
279
|
+
return { status: "held-by-other", commentId: target.id };
|
|
280
|
+
}
|
|
281
|
+
const body = renderPassOpenComment({ ...target.marker, ended_at: endedAt.toISOString() });
|
|
282
|
+
const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
|
|
283
|
+
if ("error" in updated) {
|
|
284
|
+
return { error: updated.error };
|
|
285
|
+
}
|
|
286
|
+
return { status: "cleared", commentId: target.id };
|
|
287
|
+
}
|
|
106
288
|
//# sourceMappingURL=github-lease.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PASS_MARKER_KIND } from "./constants.js";
|
|
1
2
|
import type { PlatformPrimitive } from "./tier-detection.js";
|
|
2
3
|
export declare function parseIso8601Utc(value: string): Date | null;
|
|
3
4
|
export interface ReviewOwnerLease {
|
|
@@ -33,4 +34,39 @@ export declare function findActiveLeaseComment(comments: readonly ReviewOwnerCom
|
|
|
33
34
|
now?: Date;
|
|
34
35
|
headSha?: string | null;
|
|
35
36
|
}): ReviewOwnerComment | null;
|
|
37
|
+
/**
|
|
38
|
+
* Advisory pass-open mark on an issue thread (#3607).
|
|
39
|
+
*
|
|
40
|
+
* Observe-and-mark only: it records that a pass is open, who owns it, the declared
|
|
41
|
+
* ceiling comment, and when the mark expires. It never holds, blocks, or gates
|
|
42
|
+
* another actor's write -- an arriving agent reads it and decides for itself.
|
|
43
|
+
*/
|
|
44
|
+
export interface PassOpenMarker {
|
|
45
|
+
readonly kind: typeof PASS_MARKER_KIND;
|
|
46
|
+
readonly pass_kind: string;
|
|
47
|
+
readonly owner: string;
|
|
48
|
+
readonly agent_id: string | null;
|
|
49
|
+
readonly ceiling: string | null;
|
|
50
|
+
readonly started_at: string;
|
|
51
|
+
readonly expires_at: string;
|
|
52
|
+
readonly ended_at: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface PassOpenComment {
|
|
55
|
+
readonly id: number;
|
|
56
|
+
readonly body: string;
|
|
57
|
+
readonly createdAt: string;
|
|
58
|
+
/** GitHub login that authored the comment; "" when the source did not report one. */
|
|
59
|
+
readonly authorLogin: string;
|
|
60
|
+
readonly marker: PassOpenMarker | null;
|
|
61
|
+
}
|
|
62
|
+
export declare function parsePassOpenMarker(body: string): PassOpenMarker | null;
|
|
63
|
+
export declare function renderPassOpenComment(marker: PassOpenMarker): string;
|
|
64
|
+
/** Expired or synthesis-cleared marks are ignored on read; a mark self-clears (#3607). */
|
|
65
|
+
export declare function isPassMarkerActive(marker: PassOpenMarker, now?: Date): boolean;
|
|
66
|
+
export declare function mapPassCommentEntry(entry: unknown): PassOpenComment | null;
|
|
67
|
+
/** Oldest comment id wins when two marks race, matching the lease (#2814 / #3607). */
|
|
68
|
+
export declare function selectWinningPassComment(comments: readonly PassOpenComment[]): PassOpenComment | null;
|
|
69
|
+
export declare function findActivePassComment(comments: readonly PassOpenComment[], options?: {
|
|
70
|
+
now?: Date;
|
|
71
|
+
}): PassOpenComment | null;
|
|
36
72
|
//# sourceMappingURL=lease-comment.d.ts.map
|