@event4u/agent-config 9.11.0 → 9.13.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/marketplace.json +1 -1
- package/CHANGELOG.md +80 -204
- package/README.md +8 -1
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +30 -1
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
- package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
- package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -7
- package/dist/cli-delegate/cmd_doctor.js +3 -5
- package/dist/cli-delegate/cmd_explain.js +153 -85
- package/dist/cli-delegate/cmd_export.js +5 -3
- package/dist/cli-delegate/cmd_fleet.js +2 -1
- package/dist/cli-delegate/cmd_preflight.js +6 -123
- package/dist/cli-delegate/cmd_prune.js +5 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +5 -3
- package/dist/cli-delegate/cmd_uninstall.js +6 -5
- package/dist/cli-delegate/cmd_validate.js +5 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +116 -72
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +605 -412
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +2 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/wizard-plan.js +10 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +710 -2491
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +8 -24
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +58 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +2 -2
- package/src/install/wizard-plan.ts +11 -1
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +9 -30
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/lint_ui_stack_bundles.ts +31 -0
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +93 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
- package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
- package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
|
@@ -91,6 +91,16 @@ const CLAUDE_SKILL_BUNDLE: ReadonlyArray<readonly [string, string]> = [
|
|
|
91
91
|
* — they either follow a marker-only convention (handled by bridges in
|
|
92
92
|
* Phase A6) or have no markdown convention at all.
|
|
93
93
|
*/
|
|
94
|
+
/**
|
|
95
|
+
* The one deploy-source row that carries install-time rule scoping.
|
|
96
|
+
*
|
|
97
|
+
* Exported so the CLI global path (`scripts/install.ts::_deploy_global_content`)
|
|
98
|
+
* keys its filter off the SAME string this expander does. Two paths agreeing on
|
|
99
|
+
* the predicate but disagreeing on which source it applies to would reproduce
|
|
100
|
+
* exactly the divergence rule scoping exists to prevent.
|
|
101
|
+
*/
|
|
102
|
+
export const RULE_SOURCE_REL = 'dist/agent-src/rules';
|
|
103
|
+
|
|
94
104
|
export const GLOBAL_DEPLOY_SOURCES: Readonly<Record<string, ReadonlyArray<readonly [string, string]>>> = {
|
|
95
105
|
'claude-code': CLAUDE_SKILL_BUNDLE,
|
|
96
106
|
augment: [
|
|
@@ -195,7 +205,7 @@ export function expandWizardSources(inputs: WizardExpandInputs): PlanSource[] {
|
|
|
195
205
|
// Rule sources get the install-time scoping filter — the same
|
|
196
206
|
// predicate the projection path uses (Phase 1b: the consumer
|
|
197
207
|
// install may never drift from Pipeline A, the source projection).
|
|
198
|
-
const isRuleSource = srcRel ===
|
|
208
|
+
const isRuleSource = srcRel === RULE_SOURCE_REL;
|
|
199
209
|
out.push({
|
|
200
210
|
toolId,
|
|
201
211
|
srcDir,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded shell-out for probes that parse a count or a list out of stdout.
|
|
3
|
+
*
|
|
4
|
+
* `spawnSync` buffers the child's whole stdout in memory and fails with
|
|
5
|
+
* `ENOBUFS` past `maxBuffer`, whose default is 1 MiB. A probe that treats that
|
|
6
|
+
* failure as "no output" reports **zero** — and zero, for a counting probe, is
|
|
7
|
+
* indistinguishable from a clean result. That is how a release shipped without
|
|
8
|
+
* its `Tests:` footer: the `vitest list` output had grown to 1,254,812 bytes,
|
|
9
|
+
* the probe degraded silently, and the omission surfaced only as a red gate
|
|
10
|
+
* downstream.
|
|
11
|
+
*
|
|
12
|
+
* Raising the buffer alone just moves the cliff. The invariant this module
|
|
13
|
+
* enforces is the other half: **a truncated read is an error, never a zero.**
|
|
14
|
+
*
|
|
15
|
+
* The distinction that matters:
|
|
16
|
+
*
|
|
17
|
+
* - `ENOBUFS` — the command *succeeded* and we lost its output. There is no
|
|
18
|
+
* honest count to report, so this throws.
|
|
19
|
+
* - `ENOENT` / non-zero exit — the command did not run, or ran and failed.
|
|
20
|
+
* The caller may legitimately degrade (a dev box without `npx`), so these
|
|
21
|
+
* are reported as a typed outcome rather than thrown.
|
|
22
|
+
*
|
|
23
|
+
* Scope note: this is deliberately not a general exec wrapper. It covers the
|
|
24
|
+
* counting/listing class, where a short read silently manufactures a passing
|
|
25
|
+
* result.
|
|
26
|
+
*/
|
|
27
|
+
import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default ceiling: 64 MiB.
|
|
31
|
+
*
|
|
32
|
+
* ~50× the largest listing this repository currently produces, at no cost
|
|
33
|
+
* when the output is small — `maxBuffer` is a ceiling, not an allocation.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_PROBE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/** Raised when a probe's output exceeded the buffer, so its count is unknowable. */
|
|
38
|
+
export class ProbeOverflowError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
readonly command: string,
|
|
41
|
+
readonly maxBuffer: number,
|
|
42
|
+
) {
|
|
43
|
+
super(
|
|
44
|
+
`probe \`${command}\` exceeded its ${Math.round(maxBuffer / (1024 * 1024))} MiB ` +
|
|
45
|
+
'output buffer — the read was truncated, so any count derived from it ' +
|
|
46
|
+
'would be wrong. This is an error rather than a zero on purpose: a ' +
|
|
47
|
+
'counting probe that degrades to zero reports a clean result it never ' +
|
|
48
|
+
'measured. Raise the buffer or narrow the command.',
|
|
49
|
+
);
|
|
50
|
+
this.name = 'ProbeOverflowError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ProbeOptions {
|
|
55
|
+
readonly cwd?: string;
|
|
56
|
+
readonly maxBuffer?: number;
|
|
57
|
+
readonly timeout?: number;
|
|
58
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProbeResult {
|
|
62
|
+
/** True when the command ran and exited 0. */
|
|
63
|
+
readonly ok: boolean;
|
|
64
|
+
readonly stdout: string;
|
|
65
|
+
readonly stderr: string;
|
|
66
|
+
readonly status: number | null;
|
|
67
|
+
/** Populated when the command could not be run or failed; never set for overflow (that throws). */
|
|
68
|
+
readonly failure: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run a command and capture stdout with an explicit buffer ceiling.
|
|
73
|
+
*
|
|
74
|
+
* @throws {ProbeOverflowError} when the child's output exceeded `maxBuffer`.
|
|
75
|
+
*/
|
|
76
|
+
export function runCountedProbe(
|
|
77
|
+
command: string,
|
|
78
|
+
args: readonly string[],
|
|
79
|
+
opts: ProbeOptions = {},
|
|
80
|
+
): ProbeResult {
|
|
81
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_PROBE_MAX_BUFFER;
|
|
82
|
+
const res: SpawnSyncReturns<string> = spawnSync(command, [...args], {
|
|
83
|
+
cwd: opts.cwd,
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
maxBuffer,
|
|
87
|
+
...(opts.timeout === undefined ? {} : { timeout: opts.timeout }),
|
|
88
|
+
...(opts.env === undefined ? {} : { env: opts.env }),
|
|
89
|
+
});
|
|
90
|
+
return interpretProbeResult(res, `${command} ${args.join(' ')}`.trim(), maxBuffer);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify a finished spawn. Split out from `runCountedProbe` so the failure
|
|
95
|
+
* modes are unit-testable without spawning a real, slow, large-output child.
|
|
96
|
+
*
|
|
97
|
+
* @throws {ProbeOverflowError} on `ENOBUFS`.
|
|
98
|
+
*/
|
|
99
|
+
export function interpretProbeResult(
|
|
100
|
+
res: {
|
|
101
|
+
error?: (Error & { code?: string }) | undefined;
|
|
102
|
+
status: number | null;
|
|
103
|
+
stdout: string | null;
|
|
104
|
+
stderr?: string | null;
|
|
105
|
+
},
|
|
106
|
+
command: string,
|
|
107
|
+
maxBuffer: number = DEFAULT_PROBE_MAX_BUFFER,
|
|
108
|
+
): ProbeResult {
|
|
109
|
+
const stdout = res.stdout ?? '';
|
|
110
|
+
const stderr = res.stderr ?? '';
|
|
111
|
+
if (res.error) {
|
|
112
|
+
if (res.error.code === 'ENOBUFS') {
|
|
113
|
+
throw new ProbeOverflowError(command, maxBuffer);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
stdout,
|
|
118
|
+
stderr,
|
|
119
|
+
status: res.status,
|
|
120
|
+
failure: `${res.error.code ?? res.error.message}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if ((res.status ?? 1) !== 0) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
stdout,
|
|
127
|
+
stderr,
|
|
128
|
+
status: res.status,
|
|
129
|
+
failure: `exited ${res.status ?? '(null)'}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, stdout, stderr, status: res.status, failure: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Non-empty, trimmed lines of a successful probe — the usual "list" shape. */
|
|
136
|
+
export function probeLines(result: ProbeResult): string[] {
|
|
137
|
+
if (!result.ok) return [];
|
|
138
|
+
return result.stdout
|
|
139
|
+
.split('\n')
|
|
140
|
+
.map((l) => l.trim())
|
|
141
|
+
.filter((l) => l.length > 0);
|
|
142
|
+
}
|
|
@@ -254,6 +254,13 @@ function json_string_ascii(s: string): string {
|
|
|
254
254
|
return out;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Predicate over a SOURCE file path — `true` = the file arrives in the install.
|
|
259
|
+
* Identical shape to `PlanSource.fileFilter` (`src/install/plan.ts`), so the
|
|
260
|
+
* plan path and the deploy path can share one predicate instance.
|
|
261
|
+
*/
|
|
262
|
+
export type FileFilter = (srcFile: string) => boolean;
|
|
263
|
+
|
|
257
264
|
/**
|
|
258
265
|
* Anchor-relative paths the deploy of `src` → `<anchor>/<dest_rel>` maintains
|
|
259
266
|
* — written OR skipped-as-identical.
|
|
@@ -262,8 +269,20 @@ function json_string_ascii(s: string): string {
|
|
|
262
269
|
* symlinked files resolve to file entries, symlinked subdirectories are walked
|
|
263
270
|
* through their resolved targets. `dest_rel` may be empty (`""`) for plan
|
|
264
271
|
* entries that deploy into the anchor root.
|
|
272
|
+
*
|
|
273
|
+
* `file_filter` MUST be the same predicate the copy was given. This set is what
|
|
274
|
+
* the reaper treats as "should exist"; a file the copy skipped but this set
|
|
275
|
+
* still lists is never reaped, and a file this set omits while the copy wrote it
|
|
276
|
+
* is deleted on the next deploy. Passing the filter to one and not the other is
|
|
277
|
+
* the failure mode the rule-scoping work exists to avoid — see
|
|
278
|
+
* `install._deploy_global_content`, which derives one filter and hands it to
|
|
279
|
+
* both.
|
|
265
280
|
*/
|
|
266
|
-
export function expected_deploy_files(
|
|
281
|
+
export function expected_deploy_files(
|
|
282
|
+
src: string,
|
|
283
|
+
dest_rel: string,
|
|
284
|
+
file_filter: FileFilter | null = null,
|
|
285
|
+
): Set<string> {
|
|
267
286
|
const out = new Set<string>();
|
|
268
287
|
let src_stat: fs.Stats;
|
|
269
288
|
try {
|
|
@@ -273,6 +292,9 @@ export function expected_deploy_files(src: string, dest_rel: string): Set<string
|
|
|
273
292
|
return out;
|
|
274
293
|
}
|
|
275
294
|
if (!src_stat.isDirectory()) {
|
|
295
|
+
if (file_filter !== null && !file_filter(src)) {
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
276
298
|
out.add(as_posix(dest_rel));
|
|
277
299
|
return out;
|
|
278
300
|
}
|
|
@@ -298,6 +320,12 @@ export function expected_deploy_files(src: string, dest_rel: string): Set<string
|
|
|
298
320
|
_walk(fs.realpathSync(entry), rel);
|
|
299
321
|
continue;
|
|
300
322
|
}
|
|
323
|
+
// The predicate sees the SOURCE path, never `rel`: the rule-scope
|
|
324
|
+
// predicate reads the file's own frontmatter tags, so a dest-relative
|
|
325
|
+
// path would not resolve. Same contract as the copy twin.
|
|
326
|
+
if (file_filter !== null && !file_filter(entry)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
301
329
|
out.add(as_posix(rel));
|
|
302
330
|
}
|
|
303
331
|
};
|
|
@@ -554,7 +582,15 @@ function prune_empty_dirs(prune_candidates: Set<string>, anchor_resolved: string
|
|
|
554
582
|
}
|
|
555
583
|
|
|
556
584
|
/** `anchor in node.parents` — is `anchor` a strict ancestor of `node`? */
|
|
557
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Is `node` strictly inside `anchor`? Pure path arithmetic, no I/O — callers
|
|
587
|
+
* pass already-resolved (realpath'd) paths when symlink escape matters.
|
|
588
|
+
*
|
|
589
|
+
* Exported for `install._copy_dir_dereferencing_symlinks`, which must reject a
|
|
590
|
+
* symlink whose target leaves the package root. One definition rather than a
|
|
591
|
+
* second copy of the `path.relative` idiom.
|
|
592
|
+
*/
|
|
593
|
+
export function is_ancestor(anchor: string, node: string): boolean {
|
|
558
594
|
const rel = path.relative(anchor, node);
|
|
559
595
|
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
560
596
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-contract parser + scorer for `overbuild-review-lens`.
|
|
3
|
+
*
|
|
4
|
+
* The lens's own SKILL.md states an output contract. This is the half of that
|
|
5
|
+
* contract a machine can check: tag grammar, the mandatory Chesterton's-Fence
|
|
6
|
+
* line on every `delete:` finding, the honest-null form, and the sign of the
|
|
7
|
+
* net-lines figure. Whether a live model *finds the plant* in a fixture is not
|
|
8
|
+
* checkable here — that needs a scored eval run — and the split is stated in
|
|
9
|
+
* `tests/fixtures/overbuild-lens/README.md` rather than blurred.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free and side-effect-free: parse a string, return a
|
|
12
|
+
* verdict. Nothing reads the filesystem or the network.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The six tags the lens may emit. Anything else is a contract violation. */
|
|
16
|
+
export const LENS_TAGS = ['delete', 'stdlib', 'native', 'yagni', 'shrink', 'flatten'] as const;
|
|
17
|
+
export type LensTag = (typeof LENS_TAGS)[number];
|
|
18
|
+
|
|
19
|
+
export const LENS_VERDICTS = ['lean', 'trim', 'overbuilt'] as const;
|
|
20
|
+
export type LensVerdict = (typeof LENS_VERDICTS)[number];
|
|
21
|
+
|
|
22
|
+
export interface LensFinding {
|
|
23
|
+
tag: LensTag;
|
|
24
|
+
location: string;
|
|
25
|
+
text: string;
|
|
26
|
+
/** Present only on `delete:` findings; the contract requires it there. */
|
|
27
|
+
fence?: { why: string; safe: string; covered: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParsedLensOutput {
|
|
31
|
+
verdict: LensVerdict | null;
|
|
32
|
+
findings: LensFinding[];
|
|
33
|
+
isNull: boolean;
|
|
34
|
+
netLines: number | null;
|
|
35
|
+
/** Contract violations found while parsing. Empty means the shape is legal. */
|
|
36
|
+
errors: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const _TAG_RE = new RegExp(
|
|
40
|
+
String.raw`^\s*\d+\.\s+(` + LENS_TAGS.join('|') + String.raw`):\s+(\S+)\s+(.*\S)\s*$`,
|
|
41
|
+
);
|
|
42
|
+
const _FENCE_RE =
|
|
43
|
+
/^\s*Fence:\s*why=(.+?)\s+safe=(.+?)\s+covered=(yes|no|partial)\s*$/;
|
|
44
|
+
const _VERDICT_RE = /^\s*Verdict:\s*(\S+)\s*$/;
|
|
45
|
+
const _NET_RE = /^\s*Net:\s*([+-]?\d+)\s+lines?\s*$/;
|
|
46
|
+
const _NULL_RE = /^\s*Findings:\s*none\b/;
|
|
47
|
+
const _COVERED_VALUES = new Set(['yes', 'no', 'partial']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a lens verdict block. Never throws — a malformed block comes back with
|
|
51
|
+
* a populated `errors` array, because a parser that throws on bad input cannot
|
|
52
|
+
* report *how many* ways the input was bad.
|
|
53
|
+
*/
|
|
54
|
+
export function parseLensOutput(raw: string): ParsedLensOutput {
|
|
55
|
+
const lines = raw.split('\n');
|
|
56
|
+
const errors: string[] = [];
|
|
57
|
+
const findings: LensFinding[] = [];
|
|
58
|
+
let verdict: LensVerdict | null = null;
|
|
59
|
+
let netLines: number | null = null;
|
|
60
|
+
let isNull = false;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
63
|
+
const line: string = lines[i] ?? '';
|
|
64
|
+
|
|
65
|
+
const v = line.match(_VERDICT_RE);
|
|
66
|
+
if (v) {
|
|
67
|
+
if ((LENS_VERDICTS as readonly string[]).includes(v[1] ?? '')) {
|
|
68
|
+
verdict = v[1] as LensVerdict;
|
|
69
|
+
} else {
|
|
70
|
+
errors.push(`unknown verdict \`${v[1]}\` (allowed: ${LENS_VERDICTS.join('/')})`);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const n = line.match(_NET_RE);
|
|
76
|
+
if (n) {
|
|
77
|
+
netLines = Number.parseInt(n[1] ?? '0', 10);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (_NULL_RE.test(line)) {
|
|
82
|
+
isNull = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const m = line.match(_TAG_RE);
|
|
87
|
+
if (!m) {
|
|
88
|
+
// An unrecognised numbered line is a finding the grammar rejects.
|
|
89
|
+
if (/^\s*\d+\.\s+\S+:/.test(line)) {
|
|
90
|
+
errors.push(`line ${i + 1}: not a legal tag — \`${line.trim()}\``);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const finding: LensFinding = {
|
|
96
|
+
tag: m[1] as LensTag,
|
|
97
|
+
location: m[2] ?? '',
|
|
98
|
+
text: m[3] ?? '',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (finding.tag === 'delete') {
|
|
102
|
+
const fenceLine: string = lines[i + 1] ?? '';
|
|
103
|
+
const f = fenceLine.match(_FENCE_RE);
|
|
104
|
+
if (!f) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`line ${i + 1}: \`delete:\` finding at ${finding.location} has no valid ` +
|
|
107
|
+
'Fence line (why= / safe= / covered=yes|no|partial)',
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
const covered = f[3] ?? '';
|
|
111
|
+
finding.fence = {
|
|
112
|
+
why: (f[1] ?? '').trim(),
|
|
113
|
+
safe: (f[2] ?? '').trim(),
|
|
114
|
+
covered,
|
|
115
|
+
};
|
|
116
|
+
if (!_COVERED_VALUES.has(covered)) {
|
|
117
|
+
errors.push(`line ${i + 2}: covered= must be yes|no|partial`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
findings.push(finding);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (verdict === null) errors.push('no `Verdict:` line');
|
|
126
|
+
if (netLines === null) errors.push('no `Net: <n> lines` line');
|
|
127
|
+
if (isNull && findings.length > 0) {
|
|
128
|
+
errors.push('emitted the null AND findings — pick one');
|
|
129
|
+
}
|
|
130
|
+
if (!isNull && findings.length === 0) {
|
|
131
|
+
errors.push('no findings and no null block — the null is mandatory, not optional');
|
|
132
|
+
}
|
|
133
|
+
if (verdict === 'lean' && findings.length > 0) {
|
|
134
|
+
errors.push('`lean` verdict with findings — a lean diff has nothing to cut');
|
|
135
|
+
}
|
|
136
|
+
if (verdict !== 'lean' && isNull) {
|
|
137
|
+
errors.push('null block with a non-`lean` verdict');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { verdict, findings, isNull, netLines, errors };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ExpectedLabels {
|
|
144
|
+
verdict: string;
|
|
145
|
+
must_tags: string[];
|
|
146
|
+
forbidden_tags: string[];
|
|
147
|
+
must_be_null: boolean;
|
|
148
|
+
net_sign: 'negative' | 'zero' | 'positive';
|
|
149
|
+
requires_fence?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Score a parsed output against a fixture's expected labels. Returns the list
|
|
154
|
+
* of failures — empty means the output is correct for that fixture.
|
|
155
|
+
*/
|
|
156
|
+
export function scoreAgainstExpected(
|
|
157
|
+
parsed: ParsedLensOutput,
|
|
158
|
+
expected: ExpectedLabels,
|
|
159
|
+
): string[] {
|
|
160
|
+
const failures = [...parsed.errors];
|
|
161
|
+
|
|
162
|
+
if (parsed.verdict !== expected.verdict) {
|
|
163
|
+
failures.push(`verdict ${parsed.verdict} ≠ expected ${expected.verdict}`);
|
|
164
|
+
}
|
|
165
|
+
if (parsed.isNull !== expected.must_be_null) {
|
|
166
|
+
failures.push(
|
|
167
|
+
expected.must_be_null
|
|
168
|
+
? 'expected the honest null, got findings (invented a finding on a lean diff)'
|
|
169
|
+
: 'expected findings, got the null (missed the plant)',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const seen = new Set(parsed.findings.map((f) => f.tag));
|
|
174
|
+
for (const tag of expected.must_tags) {
|
|
175
|
+
if (!seen.has(tag as LensTag)) failures.push(`missing required tag \`${tag}:\``);
|
|
176
|
+
}
|
|
177
|
+
for (const tag of expected.forbidden_tags) {
|
|
178
|
+
if (seen.has(tag as LensTag)) failures.push(`emitted forbidden tag \`${tag}:\``);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (expected.requires_fence) {
|
|
182
|
+
const unfenced = parsed.findings.filter((f) => f.tag === 'delete' && !f.fence);
|
|
183
|
+
if (unfenced.length > 0) failures.push('a `delete:` finding is missing its fence');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const sign =
|
|
187
|
+
parsed.netLines === null ? 'unknown'
|
|
188
|
+
: parsed.netLines < 0 ? 'negative'
|
|
189
|
+
: parsed.netLines > 0 ? 'positive'
|
|
190
|
+
: 'zero';
|
|
191
|
+
if (sign !== expected.net_sign) {
|
|
192
|
+
failures.push(`net-lines sign ${sign} ≠ expected ${expected.net_sign}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return failures;
|
|
196
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-aware diff scope for content checks.
|
|
3
|
+
*
|
|
4
|
+
* A release PR's own diff is version, changelog, pack metadata and marketplace
|
|
5
|
+
* files: the substantive work arrived in the commits *before* the cut. Any
|
|
6
|
+
* check scoped to "files changed in this PR" therefore measures the release
|
|
7
|
+
* wrapper, not the release. The visible consequence was a 179-commit release
|
|
8
|
+
* whose skill lint reported **"0 skills checked, INCONCLUSIVE"** and merged.
|
|
9
|
+
*
|
|
10
|
+
* The fix is a range, not a new gate: on a release PR the content checks
|
|
11
|
+
* resolve their scope to `previous_release_tag...HEAD`. Everywhere else the
|
|
12
|
+
* base is unchanged.
|
|
13
|
+
*
|
|
14
|
+
* The three release-detection primitives below were written for
|
|
15
|
+
* `self_review_gate.ts`, which already switched its own analysis range this
|
|
16
|
+
* way. They live here so the second consumer reuses the precedent instead of
|
|
17
|
+
* re-deriving it — two copies of "is this a release PR?" is exactly the drift
|
|
18
|
+
* this package has paid for before. `self_review_gate.ts` re-exports them, so
|
|
19
|
+
* its public surface is unchanged.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A release PR is detected from its own packaging diff (baseRef...HEAD,
|
|
30
|
+
* scoped to CHANGELOG.md + package.json): an added changelog heading and an
|
|
31
|
+
* added package.json version bump, agreeing on the same version. Pure over
|
|
32
|
+
* the patch text — no git call — so it is unit-testable with synthetic
|
|
33
|
+
* patches. See docs/design/release-pr-review-mode.md.
|
|
34
|
+
*/
|
|
35
|
+
export function detectReleaseVersion(patchText: string): string | null {
|
|
36
|
+
const addedLines = patchText
|
|
37
|
+
.split('\n')
|
|
38
|
+
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
|
|
39
|
+
.map((l) => l.slice(1));
|
|
40
|
+
|
|
41
|
+
let changelogVersion: string | null = null;
|
|
42
|
+
let packageVersion: string | null = null;
|
|
43
|
+
for (const line of addedLines) {
|
|
44
|
+
const changelogMatch = /^##\s*\[(\d+\.\d+\.\d+)\]/.exec(line);
|
|
45
|
+
if (changelogMatch) changelogVersion = changelogMatch[1] ?? null;
|
|
46
|
+
const packageMatch = /^\s*"version"\s*:\s*"(\d+\.\d+\.\d+)"/.exec(line);
|
|
47
|
+
if (packageMatch) packageVersion = packageMatch[1] ?? null;
|
|
48
|
+
}
|
|
49
|
+
if (changelogVersion && packageVersion && changelogVersion === packageVersion) {
|
|
50
|
+
return changelogVersion;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function releaseDetectionPatch(baseRef: string, cwd: string): string {
|
|
56
|
+
const r = spawnSync('git', ['diff', `${baseRef}...HEAD`, '--', 'CHANGELOG.md', 'package.json'], {
|
|
57
|
+
cwd,
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
60
|
+
});
|
|
61
|
+
return (r.stdout ?? '').toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Impure wrapper: collects the packaging patch via git, then detects. */
|
|
65
|
+
export function detectReleaseVersionFromGit(
|
|
66
|
+
baseRef: string,
|
|
67
|
+
cwd: string = REPO_ROOT,
|
|
68
|
+
): string | null {
|
|
69
|
+
return detectReleaseVersion(releaseDetectionPatch(baseRef, cwd));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `major.minor.patch`, tolerating an optional `v` prefix; null if not semver-shaped. */
|
|
73
|
+
function parseSemver(raw: string): [number, number, number] | null {
|
|
74
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(raw.trim());
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function semverLessThan(a: [number, number, number], b: [number, number, number]): boolean {
|
|
80
|
+
const [aMajor, aMinor, aPatch] = a;
|
|
81
|
+
const [bMajor, bMinor, bPatch] = b;
|
|
82
|
+
if (aMajor !== bMajor) return aMajor < bMajor;
|
|
83
|
+
if (aMinor !== bMinor) return aMinor < bMinor;
|
|
84
|
+
return aPatch < bPatch;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Highest semver-shaped tag strictly below `version`, or null if none. Pure
|
|
89
|
+
* over the supplied tag list — no git call. Non-semver tags (branch-backup
|
|
90
|
+
* names etc.) are ignored rather than throwing.
|
|
91
|
+
*
|
|
92
|
+
* The filtering is load-bearing, not defensive: this repository carries 152
|
|
93
|
+
* semver tags alongside names like `rebase-backup-pre-squash`, and those sort
|
|
94
|
+
* *above* every version under `git tag --sort=-v:refname`. A naive "newest
|
|
95
|
+
* tag" would resolve the scope to a backup ref.
|
|
96
|
+
*/
|
|
97
|
+
export function pickPreviousTag(version: string, tags: readonly string[]): string | null {
|
|
98
|
+
const target = parseSemver(version);
|
|
99
|
+
if (!target) return null;
|
|
100
|
+
let best: { raw: string; parsed: [number, number, number] } | null = null;
|
|
101
|
+
for (const raw of tags) {
|
|
102
|
+
const parsed = parseSemver(raw);
|
|
103
|
+
if (!parsed) continue;
|
|
104
|
+
if (!semverLessThan(parsed, target)) continue;
|
|
105
|
+
if (!best || semverLessThan(best.parsed, parsed)) best = { raw, parsed };
|
|
106
|
+
}
|
|
107
|
+
return best ? best.raw : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Local semver tags, newest first is not assumed — `pickPreviousTag` orders them. */
|
|
111
|
+
function localTags(cwd: string): string[] {
|
|
112
|
+
const r = spawnSync('git', ['tag', '--list'], { cwd, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
113
|
+
if ((r.status ?? 1) !== 0) return [];
|
|
114
|
+
return (r.stdout ?? '')
|
|
115
|
+
.split('\n')
|
|
116
|
+
.map((l) => l.trim())
|
|
117
|
+
.filter((l) => l.length > 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ContentLintScope {
|
|
121
|
+
/** The ref a diff-scoped content check should compare HEAD against. */
|
|
122
|
+
readonly base: string;
|
|
123
|
+
/** True when the current head is a release PR whose scope was widened. */
|
|
124
|
+
readonly isRelease: boolean;
|
|
125
|
+
/** The release version, when detected. */
|
|
126
|
+
readonly version: string | null;
|
|
127
|
+
/** The tag the scope widened to, when resolved. */
|
|
128
|
+
readonly previousTag: string | null;
|
|
129
|
+
/** One line naming what was resolved and why — for the check's own output. */
|
|
130
|
+
readonly reason: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ResolveOptions {
|
|
134
|
+
/** Branch a normal PR is compared against. */
|
|
135
|
+
readonly baseRef?: string;
|
|
136
|
+
readonly cwd?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Explicit override, highest precedence. Set by a caller that already
|
|
139
|
+
* knows the range (a workflow input, a `--since` flag).
|
|
140
|
+
*/
|
|
141
|
+
readonly since?: string | null | undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the diff base a content check should use.
|
|
146
|
+
*
|
|
147
|
+
* Precedence: explicit `since` → `RELEASE_LINT_BASE` env → release-PR widening
|
|
148
|
+
* → the ordinary base ref. The release branch is *not* inferred from the
|
|
149
|
+
* branch name alone: the packaging-diff detector is what proves a release, so
|
|
150
|
+
* a branch merely named `release/...` with no version bump stays on the
|
|
151
|
+
* ordinary base rather than silently widening to a whole tag span.
|
|
152
|
+
*
|
|
153
|
+
* Never throws. A release whose previous tag cannot be resolved (the very
|
|
154
|
+
* first release, or a shallow clone with no tags) falls back to the ordinary
|
|
155
|
+
* base and says so in `reason` — a check that cannot widen its scope should
|
|
156
|
+
* report a small scope honestly, not crash the release.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveContentLintScope(opts: ResolveOptions = {}): ContentLintScope {
|
|
159
|
+
const cwd = opts.cwd ?? REPO_ROOT;
|
|
160
|
+
const baseRef = opts.baseRef ?? 'origin/main';
|
|
161
|
+
|
|
162
|
+
const explicit = opts.since ?? process.env['RELEASE_LINT_BASE'] ?? null;
|
|
163
|
+
if (explicit !== null && explicit.trim() !== '') {
|
|
164
|
+
const base = explicit.trim();
|
|
165
|
+
return {
|
|
166
|
+
base,
|
|
167
|
+
isRelease: false,
|
|
168
|
+
version: null,
|
|
169
|
+
previousTag: null,
|
|
170
|
+
reason: `scope pinned to ${base} by an explicit override`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const version = detectReleaseVersionFromGit(baseRef, cwd);
|
|
175
|
+
if (version === null) {
|
|
176
|
+
return {
|
|
177
|
+
base: baseRef,
|
|
178
|
+
isRelease: false,
|
|
179
|
+
version: null,
|
|
180
|
+
previousTag: null,
|
|
181
|
+
reason: `ordinary PR — scope is ${baseRef}...HEAD`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const previousTag = pickPreviousTag(version, localTags(cwd));
|
|
186
|
+
if (previousTag === null) {
|
|
187
|
+
return {
|
|
188
|
+
base: baseRef,
|
|
189
|
+
isRelease: true,
|
|
190
|
+
version,
|
|
191
|
+
previousTag: null,
|
|
192
|
+
reason:
|
|
193
|
+
`release ${version} detected, but no earlier semver tag is reachable — ` +
|
|
194
|
+
`scope stays ${baseRef}...HEAD and therefore sees only the release wrapper`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
base: previousTag,
|
|
200
|
+
isRelease: true,
|
|
201
|
+
version,
|
|
202
|
+
previousTag,
|
|
203
|
+
reason:
|
|
204
|
+
`release ${version} detected — scope widened to ${previousTag}...HEAD ` +
|
|
205
|
+
`so the check sees the release, not the version bump`,
|
|
206
|
+
};
|
|
207
|
+
}
|