@uniqbit/mate-core 0.18.4 → 1.0.0-canary.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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
import type { CapabilityConfig } from "../orchestrator/types";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export type ProtectionReason = "default" | "pattern";
|
|
9
|
+
|
|
10
|
+
export interface BranchProtection {
|
|
11
|
+
protected: boolean;
|
|
12
|
+
reason: ProtectionReason | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProtectionOptions {
|
|
16
|
+
defaultBranch: string;
|
|
17
|
+
/** Configured glob patterns; `*` stays within one path segment, `**` crosses segments. */
|
|
18
|
+
patterns: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function globToRegExp(pattern: string): RegExp {
|
|
22
|
+
const source = pattern
|
|
23
|
+
.split("**")
|
|
24
|
+
.map((part) =>
|
|
25
|
+
part
|
|
26
|
+
.split("*")
|
|
27
|
+
.map((literal) => literal.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
28
|
+
.join("[^/]*"),
|
|
29
|
+
)
|
|
30
|
+
.join(".*");
|
|
31
|
+
return new RegExp(`^${source}$`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A branch is protected when it is the default branch or matches a pattern. */
|
|
35
|
+
export function classifyBranch(branch: string, options: ProtectionOptions): BranchProtection {
|
|
36
|
+
if (branch === options.defaultBranch) return { protected: true, reason: "default" };
|
|
37
|
+
if (options.patterns.some((pattern) => globToRegExp(pattern).test(branch))) {
|
|
38
|
+
return { protected: true, reason: "pattern" };
|
|
39
|
+
}
|
|
40
|
+
return { protected: false, reason: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Protected-branch patterns from the companion's openspec capability config. */
|
|
44
|
+
export function configuredProtectedPatterns(capabilities: readonly CapabilityConfig[]): string[] {
|
|
45
|
+
const openspec = capabilities.find((capability) => capability.name === "openspec");
|
|
46
|
+
const patterns = openspec?.protectedBranches;
|
|
47
|
+
return Array.isArray(patterns)
|
|
48
|
+
? patterns.filter((pattern): pattern is string => typeof pattern === "string")
|
|
49
|
+
: [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read-only local Git queries against a Working Repository. Nothing here touches the network. */
|
|
53
|
+
export interface WorkingRepoGit {
|
|
54
|
+
/** Checked-out branch, or null in a detached HEAD. */
|
|
55
|
+
currentBranch(): Promise<string | null>;
|
|
56
|
+
/** `origin/HEAD`, then an existing `origin/main` or `origin/master`, then `init.defaultBranch`, then `main`. */
|
|
57
|
+
defaultBranch(): Promise<string>;
|
|
58
|
+
/** The `origin` remote URL, or null without one. */
|
|
59
|
+
remoteUrl(): Promise<string | null>;
|
|
60
|
+
/** One-line summaries of commits on HEAD that no remote-tracking branch contains. */
|
|
61
|
+
unpushedCommits(): Promise<string[]>;
|
|
62
|
+
/** True when the branch exists locally or as `origin/<branch>`. */
|
|
63
|
+
branchExists(branch: string): Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function workingRepoGit(repoPath: string): WorkingRepoGit {
|
|
67
|
+
const run = async (...args: string[]): Promise<string | null> => {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await execFileAsync("git", args, { cwd: repoPath });
|
|
70
|
+
return stdout.trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const remoteTip = async (branch: string): Promise<string | null> =>
|
|
77
|
+
(await run("rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`)) || null;
|
|
78
|
+
const defaultBranch = async (): Promise<string> => {
|
|
79
|
+
const remoteHead = await run("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD");
|
|
80
|
+
if (remoteHead) return remoteHead.replace(/^origin\//, "");
|
|
81
|
+
for (const candidate of ["main", "master"]) {
|
|
82
|
+
// oxlint-disable-next-line no-await-in-loop -- the steps are a decision chain
|
|
83
|
+
if (await remoteTip(candidate)) return candidate;
|
|
84
|
+
}
|
|
85
|
+
return (await run("config", "--get", "init.defaultBranch")) || "main";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const currentBranch = async (): Promise<string | null> =>
|
|
89
|
+
(await run("symbolic-ref", "--quiet", "--short", "HEAD")) || null;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
currentBranch,
|
|
93
|
+
defaultBranch,
|
|
94
|
+
async remoteUrl() {
|
|
95
|
+
return (await run("remote", "get-url", "origin")) || null;
|
|
96
|
+
},
|
|
97
|
+
async unpushedCommits() {
|
|
98
|
+
const log = await run("log", "--oneline", "HEAD", "--not", "--remotes");
|
|
99
|
+
return log ? log.split("\n").filter(Boolean) : [];
|
|
100
|
+
},
|
|
101
|
+
async branchExists(branch: string) {
|
|
102
|
+
for (const ref of [`refs/heads/${branch}`, `refs/remotes/origin/${branch}`]) {
|
|
103
|
+
if ((await run("rev-parse", "--verify", "--quiet", ref)) !== null) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { WorkingRepoGit } from "./branches";
|
|
4
|
+
import type { ForgeRepo } from "./forge";
|
|
5
|
+
|
|
6
|
+
export interface WorkingRepoInfo {
|
|
7
|
+
path: string;
|
|
8
|
+
/** Full repository path from the `origin` remote (`acme/platform/api`), or null without one. */
|
|
9
|
+
slug: string | null;
|
|
10
|
+
forgeRepo: ForgeRepo | null;
|
|
11
|
+
git: WorkingRepoGit;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The companion, the Working Repository with its remote identity, and protected-branch patterns. */
|
|
15
|
+
export interface DeliveryEnvironment {
|
|
16
|
+
companionPath: string;
|
|
17
|
+
workingRepo: WorkingRepoInfo | null;
|
|
18
|
+
protectedPatterns: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Host of a repository other than the Working Repository, from a linked checkout's
|
|
21
|
+
* `origin` remote, else the Working Repository's host; null when neither is known.
|
|
22
|
+
*/
|
|
23
|
+
hostOf?: (repository: string) => Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `dir` relative to the companion, with forward slashes. */
|
|
27
|
+
export function companionRelative(environment: DeliveryEnvironment, dir: string): string {
|
|
28
|
+
return path.relative(environment.companionPath, dir).split(path.sep).join("/");
|
|
29
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge adapters answer read-only questions about a repository and its Delivery Branch:
|
|
3
|
+
* the default branch, whether the branch is protected, and which PR has it as head. GitHub (github.com) is the only adapter; every other host
|
|
4
|
+
* falls back to asking the user. Every failure is returned, never thrown, and the token
|
|
5
|
+
* is scrubbed from anything a failure carries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { errorMessage } from "../../runtime/error-message";
|
|
9
|
+
import { pickProvenance, type MergeProvenance, type MergeState } from "./record";
|
|
10
|
+
|
|
11
|
+
export interface ForgeRepo {
|
|
12
|
+
host: string;
|
|
13
|
+
/** Every path segment before the repository; nested groups keep theirs (`acme/platform`). */
|
|
14
|
+
owner: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PullCandidate {
|
|
19
|
+
number: number;
|
|
20
|
+
url: string;
|
|
21
|
+
state: MergeState;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PullDetail extends MergeProvenance {
|
|
25
|
+
number: number;
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PullClassification =
|
|
30
|
+
| { kind: "merged"; pr: PullDetail }
|
|
31
|
+
| { kind: "open"; pr: PullDetail }
|
|
32
|
+
| { kind: "closed-unmerged"; pr: PullDetail }
|
|
33
|
+
| { kind: "ambiguous"; candidates: PullCandidate[] }
|
|
34
|
+
| { kind: "none" };
|
|
35
|
+
|
|
36
|
+
export type ForgeFailureReason = "permission" | "rate-limit" | "auth" | "network" | "unexpected";
|
|
37
|
+
|
|
38
|
+
export interface ForgeFailure {
|
|
39
|
+
reason: ForgeFailureReason;
|
|
40
|
+
message: string;
|
|
41
|
+
/** The fine-grained read permission a `permission` failure lacks. */
|
|
42
|
+
permission?: string;
|
|
43
|
+
/** The HTTP status the forge answered, when it answered. */
|
|
44
|
+
status?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ForgeOutcome<T> = { ok: true; value: T } | { ok: false; failure: ForgeFailure };
|
|
48
|
+
|
|
49
|
+
export interface ForgeAdapter {
|
|
50
|
+
id: string;
|
|
51
|
+
defaultBranch(repo: ForgeRepo): Promise<ForgeOutcome<string>>;
|
|
52
|
+
/** Classifies the PRs whose head is `branch`; `prNumber` picks one of several. */
|
|
53
|
+
pullsForBranch(
|
|
54
|
+
repo: ForgeRepo,
|
|
55
|
+
branch: string,
|
|
56
|
+
prNumber?: number,
|
|
57
|
+
): Promise<ForgeOutcome<PullClassification>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type FetchLike = (
|
|
61
|
+
url: string,
|
|
62
|
+
init?: { headers?: Record<string, string> },
|
|
63
|
+
) => Promise<Response>;
|
|
64
|
+
|
|
65
|
+
export interface ForgeDeps {
|
|
66
|
+
fetch?: FetchLike;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parses SSH, scp-style, and HTTPS remotes; credentials in a URL are discarded. */
|
|
70
|
+
export function parseRemote(url: string): ForgeRepo | null {
|
|
71
|
+
const cleaned = url.trim();
|
|
72
|
+
let host: string;
|
|
73
|
+
let remotePath: string;
|
|
74
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(cleaned)) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(cleaned);
|
|
77
|
+
host = parsed.hostname;
|
|
78
|
+
remotePath = parsed.pathname;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// `user@` is optional: an SSH config alias such as `work:org/repo` has none.
|
|
84
|
+
const scp = /^(?:[^/\s@]+@)?([^/\s:]{2,}):(.+)$/.exec(cleaned);
|
|
85
|
+
if (!scp) return null;
|
|
86
|
+
host = scp[1];
|
|
87
|
+
remotePath = scp[2];
|
|
88
|
+
}
|
|
89
|
+
if (!host) return null;
|
|
90
|
+
return forgeRepoOf(host, remotePath.replace(/\.git\/?$/, ""));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Splits a repository path at its last segment; null below two segments. */
|
|
94
|
+
export function forgeRepoOf(host: string, path: string): ForgeRepo | null {
|
|
95
|
+
const parts = path.split("/").filter(Boolean);
|
|
96
|
+
if (parts.length < 2) return null;
|
|
97
|
+
return {
|
|
98
|
+
host: host.toLowerCase(),
|
|
99
|
+
owner: parts.slice(0, -1).join("/"),
|
|
100
|
+
repo: parts.at(-1) as string,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The repository's identity: its full path on the host, `owner/repo`. */
|
|
105
|
+
export function repositoryPath(repo: ForgeRepo): string {
|
|
106
|
+
return `${repo.owner}/${repo.repo}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The adapter for a remote's host, or null when the host has none. */
|
|
110
|
+
export function forgeFor(
|
|
111
|
+
repo: ForgeRepo,
|
|
112
|
+
token: string,
|
|
113
|
+
deps: ForgeDeps = {},
|
|
114
|
+
): ForgeAdapter | null {
|
|
115
|
+
return repo.host === "github.com" ? githubForge(token, deps) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Replaces every occurrence of the token, so no failure text can carry it. */
|
|
119
|
+
function redactToken(text: string, token: string): string {
|
|
120
|
+
return token ? text.split(token).join("[redacted]") : text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const GITHUB_API = "https://api.github.com";
|
|
124
|
+
|
|
125
|
+
interface GithubPull {
|
|
126
|
+
number: number;
|
|
127
|
+
html_url: string;
|
|
128
|
+
state: string;
|
|
129
|
+
merged_at: string | null;
|
|
130
|
+
merge_commit_sha?: string | null;
|
|
131
|
+
merged_by?: { login?: string } | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function candidateState(pull: GithubPull): PullCandidate["state"] {
|
|
135
|
+
if (pull.merged_at) return "merged";
|
|
136
|
+
return pull.state === "open" ? "open" : "closed-unmerged";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detail(pull: GithubPull): PullDetail {
|
|
140
|
+
return {
|
|
141
|
+
number: pull.number,
|
|
142
|
+
url: pull.html_url,
|
|
143
|
+
...pickProvenance({
|
|
144
|
+
mergedAt: pull.merged_at ?? undefined,
|
|
145
|
+
mergeCommit: (pull.merged_at && pull.merge_commit_sha) || undefined,
|
|
146
|
+
mergedBy: pull.merged_by?.login,
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function repoPath(repo: ForgeRepo): string {
|
|
152
|
+
return `/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function githubForge(token: string, deps: ForgeDeps = {}): ForgeAdapter {
|
|
156
|
+
const fetchFn: FetchLike = deps.fetch ?? ((url, init) => fetch(url, init));
|
|
157
|
+
|
|
158
|
+
async function get<T>(apiPath: string, permission: string): Promise<ForgeOutcome<T>> {
|
|
159
|
+
let response: Response;
|
|
160
|
+
try {
|
|
161
|
+
response = await fetchFn(`${GITHUB_API}${apiPath}`, {
|
|
162
|
+
headers: {
|
|
163
|
+
accept: "application/vnd.github+json",
|
|
164
|
+
authorization: `Bearer ${token}`,
|
|
165
|
+
"x-github-api-version": "2022-11-28",
|
|
166
|
+
"user-agent": "mate-cli",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
failure: {
|
|
173
|
+
reason: "network",
|
|
174
|
+
message: redactToken(`GitHub request failed: ${errorMessage(error)}`, token),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const fail = (
|
|
179
|
+
reason: ForgeFailureReason,
|
|
180
|
+
message: string,
|
|
181
|
+
extra: Pick<ForgeFailure, "permission"> = {},
|
|
182
|
+
): ForgeOutcome<T> => ({
|
|
183
|
+
ok: false,
|
|
184
|
+
failure: {
|
|
185
|
+
reason,
|
|
186
|
+
message: redactToken(message, token),
|
|
187
|
+
...extra,
|
|
188
|
+
...(response.ok ? {} : { status: response.status }),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
if (response.ok) {
|
|
192
|
+
try {
|
|
193
|
+
return { ok: true, value: (await response.json()) as T };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return fail(
|
|
196
|
+
"unexpected",
|
|
197
|
+
`GitHub answered with an unreadable body: ${errorMessage(error)}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (response.status === 401) return fail("auth", "GitHub rejected the token (401).");
|
|
202
|
+
if (response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0") {
|
|
203
|
+
return fail("rate-limit", "GitHub API rate limit reached (403).");
|
|
204
|
+
}
|
|
205
|
+
if (response.status === 403 || response.status === 404) {
|
|
206
|
+
return fail(
|
|
207
|
+
"permission",
|
|
208
|
+
`GitHub refused the request (${response.status}); the token needs ${permission} on this repository.`,
|
|
209
|
+
{ permission },
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return fail("unexpected", `GitHub answered ${response.status}.`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
id: "github",
|
|
217
|
+
async defaultBranch(repo) {
|
|
218
|
+
const result = await get<{ default_branch?: string }>(repoPath(repo), "Metadata: read");
|
|
219
|
+
if (!result.ok) return result;
|
|
220
|
+
const branch = result.value.default_branch;
|
|
221
|
+
return branch
|
|
222
|
+
? { ok: true, value: branch }
|
|
223
|
+
: {
|
|
224
|
+
ok: false,
|
|
225
|
+
failure: { reason: "unexpected", message: "GitHub named no default branch." },
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
async pullsForBranch(repo, branch, prNumber) {
|
|
229
|
+
const head = encodeURIComponent(`${repo.owner}:${branch}`);
|
|
230
|
+
const listPulls = (target: string) =>
|
|
231
|
+
get<GithubPull[]>(
|
|
232
|
+
`${target}/pulls?head=${head}&state=all&per_page=100`,
|
|
233
|
+
"Pull requests: read",
|
|
234
|
+
);
|
|
235
|
+
let listed = await listPulls(repoPath(repo));
|
|
236
|
+
if (!listed.ok) return listed;
|
|
237
|
+
let pullRepo = repoPath(repo);
|
|
238
|
+
if (listed.value.length === 0) {
|
|
239
|
+
// A fork's PR is opened on its parent; the head stays `<fork owner>:<branch>`.
|
|
240
|
+
const metadata = await get<{ parent?: { full_name?: string } }>(
|
|
241
|
+
repoPath(repo),
|
|
242
|
+
"Metadata: read",
|
|
243
|
+
);
|
|
244
|
+
const parent = metadata.ok ? metadata.value.parent?.full_name : undefined;
|
|
245
|
+
if (parent) {
|
|
246
|
+
pullRepo = `/repos/${parent.split("/").map(encodeURIComponent).join("/")}`;
|
|
247
|
+
listed = await listPulls(pullRepo);
|
|
248
|
+
if (!listed.ok) return listed;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const pulls =
|
|
252
|
+
prNumber === undefined
|
|
253
|
+
? listed.value
|
|
254
|
+
: listed.value.filter((entry) => entry.number === prNumber);
|
|
255
|
+
if (pulls.length === 0) return { ok: true, value: { kind: "none" } };
|
|
256
|
+
if (pulls.length > 1) {
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
value: {
|
|
260
|
+
kind: "ambiguous",
|
|
261
|
+
candidates: pulls
|
|
262
|
+
.map((entry) => ({
|
|
263
|
+
number: entry.number,
|
|
264
|
+
url: entry.html_url,
|
|
265
|
+
state: candidateState(entry),
|
|
266
|
+
}))
|
|
267
|
+
.toSorted((a, b) => a.number - b.number),
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const [only] = pulls;
|
|
272
|
+
const state = candidateState(only);
|
|
273
|
+
if (state !== "merged") return { ok: true, value: { kind: state, pr: detail(only) } };
|
|
274
|
+
// The list omits the merger; only the single-PR endpoint carries it.
|
|
275
|
+
const full = await get<GithubPull>(`${pullRepo}/pulls/${only.number}`, "Pull requests: read");
|
|
276
|
+
return { ok: true, value: { kind: "merged", pr: detail(full.ok ? full.value : only) } };
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
activeChanges,
|
|
6
|
+
archiveAnchors,
|
|
7
|
+
changeNameOf,
|
|
8
|
+
changesDirOf,
|
|
9
|
+
deltaSpecs,
|
|
10
|
+
} from "../change-layout";
|
|
11
|
+
import { readFrontmatter } from "../frontmatter";
|
|
12
|
+
import { isMateSchema } from "./stages";
|
|
13
|
+
|
|
14
|
+
export interface RequirementEdit {
|
|
15
|
+
capability: string;
|
|
16
|
+
requirement: string;
|
|
17
|
+
operation: "MODIFIED" | "REMOVED";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DependencyWarning = { kind: "dependency"; change: string; message: string };
|
|
21
|
+
|
|
22
|
+
export type PlanningWarning = {
|
|
23
|
+
kind: "incomplete-planning";
|
|
24
|
+
change: string;
|
|
25
|
+
artifacts: string[];
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ArchiveWarning =
|
|
30
|
+
| { kind: "overlap"; change: string; capability: string; requirement: string; message: string }
|
|
31
|
+
| DependencyWarning
|
|
32
|
+
| PlanningWarning;
|
|
33
|
+
|
|
34
|
+
const SECTION = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/;
|
|
35
|
+
const REQUIREMENT = /^###\s+Requirement:\s*(.+?)\s*$/;
|
|
36
|
+
const TASK = /^\s*[-*]\s+\[([ xX])\]/;
|
|
37
|
+
|
|
38
|
+
/** Requirements a change's delta specs MODIFY or REMOVE, by full capability path. */
|
|
39
|
+
export async function deltaRequirementEdits(changeDir: string): Promise<RequirementEdit[]> {
|
|
40
|
+
const edits: RequirementEdit[] = [];
|
|
41
|
+
for (const { file, capability } of await deltaSpecs(changeDir)) {
|
|
42
|
+
if (capability === null) continue;
|
|
43
|
+
let source: string;
|
|
44
|
+
try {
|
|
45
|
+
source = await fs.readFile(path.join(changeDir, "specs", file), "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let section: string | null = null;
|
|
50
|
+
for (const line of source.split(/\r?\n/)) {
|
|
51
|
+
const heading = SECTION.exec(line);
|
|
52
|
+
if (heading) {
|
|
53
|
+
section = heading[1];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (line.startsWith("## ")) section = null;
|
|
57
|
+
const requirement = REQUIREMENT.exec(line);
|
|
58
|
+
if (requirement && (section === "MODIFIED" || section === "REMOVED")) {
|
|
59
|
+
edits.push({ capability, requirement: requirement[1], operation: section });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return edits;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Change names listed under `dependsOn` in any of the change's artifact frontmatter. */
|
|
67
|
+
export async function declaredDependencies(changeDir: string): Promise<string[]> {
|
|
68
|
+
let files: string[];
|
|
69
|
+
try {
|
|
70
|
+
files = (await fs.readdir(changeDir)).filter((file) => file.endsWith(".md"));
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const names = new Set<string>();
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
// A malformed header declares nothing; validation reports it elsewhere.
|
|
77
|
+
const dependsOn = readFrontmatter(
|
|
78
|
+
await fs.readFile(path.join(changeDir, file), "utf8"),
|
|
79
|
+
)?.dependsOn;
|
|
80
|
+
for (const name of Array.isArray(dependsOn) ? dependsOn : [dependsOn]) {
|
|
81
|
+
if (typeof name === "string" && name.trim()) names.add(name.trim());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...names].toSorted();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function archivedChangeNames(companionPath: string): Promise<Set<string>> {
|
|
88
|
+
return new Set((await archiveAnchors(companionPath)).map(changeNameOf));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when the change's `tasks.md` has tasks and every one is checked. */
|
|
92
|
+
async function tasksComplete(changeDir: string): Promise<boolean> {
|
|
93
|
+
let source: string;
|
|
94
|
+
try {
|
|
95
|
+
source = await fs.readFile(path.join(changeDir, "tasks.md"), "utf8");
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const marks = source.split(/\r?\n/).flatMap((line) => TASK.exec(line)?.[1] ?? []);
|
|
100
|
+
return marks.length > 0 && marks.every((mark) => mark !== " ");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Warnings shown before applying `change`: declared dependencies neither archived nor fully
|
|
105
|
+
* implemented, so the change would be built without the code it builds on. Never blocks.
|
|
106
|
+
*/
|
|
107
|
+
export async function applyWarnings(
|
|
108
|
+
companionPath: string,
|
|
109
|
+
change: string,
|
|
110
|
+
): Promise<DependencyWarning[]> {
|
|
111
|
+
const changesDir = changesDirOf(companionPath);
|
|
112
|
+
const archived = await archivedChangeNames(companionPath);
|
|
113
|
+
const warnings: DependencyWarning[] = [];
|
|
114
|
+
for (const dependency of await declaredDependencies(path.join(changesDir, change))) {
|
|
115
|
+
if (archived.has(dependency) || (await tasksComplete(path.join(changesDir, dependency)))) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
warnings.push({
|
|
119
|
+
kind: "dependency",
|
|
120
|
+
change: dependency,
|
|
121
|
+
message: `${change} depends on ${dependency}, which is not implemented yet; apply ${dependency} first.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return warnings;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Warning for schema artifacts that `openspec status --change <change> --json` reports neither
|
|
129
|
+
* done nor skipped, e.g. a design missing after a `mate-minimal` change moved to `mate-v1`.
|
|
130
|
+
* Only Mate's schemas require every artifact; OpenSpec reports another schema's conditional
|
|
131
|
+
* artifacts (spec-driven's design) as `ready`, so those yield no warning, as does unparsable output.
|
|
132
|
+
*/
|
|
133
|
+
export function planningWarning(change: string, statusJson: string): PlanningWarning | null {
|
|
134
|
+
let status: { schemaName?: unknown; artifacts?: unknown };
|
|
135
|
+
try {
|
|
136
|
+
status = JSON.parse(statusJson) as typeof status;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const { schemaName, artifacts } = status ?? {};
|
|
141
|
+
if (typeof schemaName !== "string" || !isMateSchema(schemaName)) return null;
|
|
142
|
+
if (!Array.isArray(artifacts)) return null;
|
|
143
|
+
const missing = artifacts
|
|
144
|
+
.filter((artifact): artifact is { id: string; status?: unknown } => {
|
|
145
|
+
return typeof artifact?.id === "string";
|
|
146
|
+
})
|
|
147
|
+
.filter(({ status }) => status !== "done" && status !== "skipped")
|
|
148
|
+
.map(({ id }) => id);
|
|
149
|
+
if (missing.length === 0) return null;
|
|
150
|
+
return {
|
|
151
|
+
kind: "incomplete-planning",
|
|
152
|
+
change,
|
|
153
|
+
artifacts: missing,
|
|
154
|
+
message: `${change} is missing planning artifacts its schema requires: ${missing.join(", ")}.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requirementKey(edit: RequirementEdit): string {
|
|
159
|
+
return JSON.stringify([edit.capability, edit.requirement]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Warnings shown before archiving `change`: another active change MODIFYing or REMOVing a
|
|
164
|
+
* requirement this change also MODIFIES or REMOVES, and declared dependencies that are not
|
|
165
|
+
* archived yet. Warnings inform; they never block archive.
|
|
166
|
+
*/
|
|
167
|
+
export async function archiveWarnings(
|
|
168
|
+
companionPath: string,
|
|
169
|
+
change: string,
|
|
170
|
+
): Promise<ArchiveWarning[]> {
|
|
171
|
+
const changesDir = changesDirOf(companionPath);
|
|
172
|
+
const warnings: ArchiveWarning[] = [];
|
|
173
|
+
|
|
174
|
+
const own = await deltaRequirementEdits(path.join(changesDir, change));
|
|
175
|
+
if (own.length > 0) {
|
|
176
|
+
const ownKeys = new Set(own.map(requirementKey));
|
|
177
|
+
for (const other of await activeChanges(companionPath)) {
|
|
178
|
+
if (other === change) continue;
|
|
179
|
+
for (const edit of await deltaRequirementEdits(path.join(changesDir, other))) {
|
|
180
|
+
if (!ownKeys.has(requirementKey(edit))) continue;
|
|
181
|
+
warnings.push({
|
|
182
|
+
kind: "overlap",
|
|
183
|
+
change: other,
|
|
184
|
+
capability: edit.capability,
|
|
185
|
+
requirement: edit.requirement,
|
|
186
|
+
message: `${other} also changes ${edit.capability} › ${edit.requirement}; archiving ${other} later will replace this change's version of that requirement.`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const archived = await archivedChangeNames(companionPath);
|
|
193
|
+
for (const dependency of await declaredDependencies(path.join(changesDir, change))) {
|
|
194
|
+
if (archived.has(dependency)) continue;
|
|
195
|
+
warnings.push({
|
|
196
|
+
kind: "dependency",
|
|
197
|
+
change: dependency,
|
|
198
|
+
message: `${change} depends on ${dependency}, which is not archived yet; archive ${dependency} first.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return warnings;
|
|
202
|
+
}
|