@webpieces/pr-gate 0.4.507 → 0.4.508
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/package.json +2 -2
- package/src/scripts/commands/review-upsert-pr-command.d.ts +3 -1
- package/src/scripts/commands/review-upsert-pr-command.js +10 -1
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/build-artifact-gate.d.ts +80 -0
- package/src/scripts/workflow/build-artifact-gate.js +178 -0
- package/src/scripts/workflow/build-artifact-gate.js.map +1 -0
- package/src/scripts/workflow/generated-artifact-registry.d.ts +59 -0
- package/src/scripts/workflow/generated-artifact-registry.js +147 -0
- package/src/scripts/workflow/generated-artifact-registry.js.map +1 -0
- package/src/scripts/workflow/git-exec.d.ts +11 -0
- package/src/scripts/workflow/git-exec.js +17 -1
- package/src/scripts/workflow/git-exec.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.508",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.508",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RepoRootFinder, ChecklistInstructionsService, ReviewerInstructionsService } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BuildAffected } from '../workflow/build-affected';
|
|
4
|
+
import { BuildArtifactGate } from '../workflow/build-artifact-gate';
|
|
4
5
|
import { ChecklistNotice } from '../workflow/checklist-notice';
|
|
5
6
|
import { ChecklistScanner } from '../workflow/checklist-scanner';
|
|
6
7
|
import { DiffMaterializer } from '../workflow/diff-materializer';
|
|
@@ -37,6 +38,7 @@ export declare class ReviewUpsertPrCommand {
|
|
|
37
38
|
private readonly aiBranchName;
|
|
38
39
|
private readonly gitExec;
|
|
39
40
|
private readonly buildAffected;
|
|
41
|
+
private readonly buildArtifactGate;
|
|
40
42
|
private readonly mergeState;
|
|
41
43
|
private readonly mergeEnd;
|
|
42
44
|
private readonly checklistScanner;
|
|
@@ -46,7 +48,7 @@ export declare class ReviewUpsertPrCommand {
|
|
|
46
48
|
private readonly briefingBuilder;
|
|
47
49
|
private readonly reviewerInstructions;
|
|
48
50
|
private readonly receipts;
|
|
49
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner, checklistNotice: ChecklistNotice, instructions: ChecklistInstructionsService, materializer: DiffMaterializer, briefingBuilder: ReviewerBriefingBuilder, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService);
|
|
51
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, buildArtifactGate: BuildArtifactGate, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner, checklistNotice: ChecklistNotice, instructions: ChecklistInstructionsService, materializer: DiffMaterializer, briefingBuilder: ReviewerBriefingBuilder, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService);
|
|
50
52
|
run(): Promise<void>;
|
|
51
53
|
/**
|
|
52
54
|
* Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from
|
|
@@ -8,6 +8,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
8
8
|
const inversify_1 = require("inversify");
|
|
9
9
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
10
10
|
const build_affected_1 = require("../workflow/build-affected");
|
|
11
|
+
const build_artifact_gate_1 = require("../workflow/build-artifact-gate");
|
|
11
12
|
const checklist_notice_1 = require("../workflow/checklist-notice");
|
|
12
13
|
const checklist_scanner_1 = require("../workflow/checklist-scanner");
|
|
13
14
|
const diff_materializer_1 = require("../workflow/diff-materializer");
|
|
@@ -46,6 +47,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
46
47
|
aiBranchName;
|
|
47
48
|
gitExec;
|
|
48
49
|
buildAffected;
|
|
50
|
+
buildArtifactGate;
|
|
49
51
|
mergeState;
|
|
50
52
|
mergeEnd;
|
|
51
53
|
checklistScanner;
|
|
@@ -56,11 +58,12 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
56
58
|
reviewerInstructions;
|
|
57
59
|
receipts;
|
|
58
60
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
59
|
-
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, mergeState, mergeEnd, checklistScanner, checklistNotice, instructions, materializer, briefingBuilder, reviewerInstructions, receipts) {
|
|
61
|
+
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner, checklistNotice, instructions, materializer, briefingBuilder, reviewerInstructions, receipts) {
|
|
60
62
|
this.repoRootFinder = repoRootFinder;
|
|
61
63
|
this.aiBranchName = aiBranchName;
|
|
62
64
|
this.gitExec = gitExec;
|
|
63
65
|
this.buildAffected = buildAffected;
|
|
66
|
+
this.buildArtifactGate = buildArtifactGate;
|
|
64
67
|
this.mergeState = mergeState;
|
|
65
68
|
this.mergeEnd = mergeEnd;
|
|
66
69
|
this.checklistScanner = checklistScanner;
|
|
@@ -115,6 +118,11 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
115
118
|
*/
|
|
116
119
|
runBuildGate(repoRoot) {
|
|
117
120
|
this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (pre-review)', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.'));
|
|
121
|
+
// Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in
|
|
122
|
+
// finish, because this is the stage that ran buildCommand and is therefore holding the dirty
|
|
123
|
+
// tree at the exact moment the question is answerable. Replaces the per-project
|
|
124
|
+
// validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.
|
|
125
|
+
this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);
|
|
118
126
|
return new Date().toISOString();
|
|
119
127
|
}
|
|
120
128
|
/**
|
|
@@ -231,6 +239,7 @@ exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand = tslib_1.__decorate([
|
|
|
231
239
|
git_readAiBranchName_1.AiBranchName,
|
|
232
240
|
git_exec_1.GitExec,
|
|
233
241
|
build_affected_1.BuildAffected,
|
|
242
|
+
build_artifact_gate_1.BuildArtifactGate,
|
|
234
243
|
merge_state_1.MergeState,
|
|
235
244
|
merge_end_1.MergeEnd,
|
|
236
245
|
checklist_scanner_1.ChecklistScanner,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, reviewJsonSchemaHint, writeTemplate, PrGateConfig, RepoRootFinder,\n ChecklistInstructionsService, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffManifest, DiffManifestEntry, DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly checklistNotice: ChecklistNotice,\n private readonly instructions: ChecklistInstructionsService,\n private readonly materializer: DiffMaterializer,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false));\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (pre-review)',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Re-point pr-context.json at the extracted diff now that there is one — the scan wrote it before\n // materialization, so without this the reviewers' own context file would not mention the diff dir.\n this.rewritePrContext(repoRoot, featureName, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n this.printDiffSummary(manifest, diffDir);\n return briefings;\n }\n\n // pr-context.json is written by the scan (before the diff exists). Patch in the dir rather than\n // re-deriving the whole context: re-deriving means a second basis resolution, which is the drift.\n private rewritePrContext(repoRoot: string, featureName: string, diffDir: string): void {\n const p = path.join(repoRoot, '.webpieces', 'pr-review', featureName, 'pr-context.json');\n if (!fs.existsSync(p)) return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context is left as-is, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, one key added\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n raw['diffDir'] = diffDir;\n fs.writeFileSync(p, JSON.stringify(raw, null, 2) + '\\n');\n } catch (err: unknown) {\n const error = toError(err);\n void error; // leave the file as-is — the instructions files carry the same paths directly\n }\n }\n\n private printDiffSummary(manifest: DiffManifest, diffDir: string): void {\n process.stdout.write('\\n' + SEP + '② Diff extracted for the reviewers\\n' + SEP + '\\n');\n process.stdout.write(` ${manifest.entries.length} file(s) → ${diffDir}\\n`);\n if (manifest.excluded.length > 0) {\n process.stdout.write(` ${manifest.excluded.length} excluded by pr-gate.reviewDiffExclude (stubbed, still matched)\\n`);\n }\n const truncated = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated).length;\n if (truncated > 0) process.stdout.write(` ${truncated} truncated at the per-file cap (each says so in its footer)\\n`);\n }\n\n /** What the AI must do next: spawn each reviewer, then write review.json. */\n private report(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): void {\n process.stdout.write('\\n' + SEP + '③ Review, then finish\\n' + SEP + '\\n');\n if (scan.applicable.length === 0) {\n process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));\n } else {\n process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));\n }\n process.stdout.write(\n `\\nThen review your own changes and\\n${reviewJsonSchemaHint(scan.reviewPath)}\\n\\n` +\n `Finally run: pnpm wp-finish-upsert-pr\\n` +\n `(The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n`);\n }\n\n /**\n * One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the\n * generated instructions file is the contract, so anything restated here is a second copy that can go\n * stale — which is exactly how a removed `success` field outlived its own removal in print.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private spawnBlocks(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): string {\n const reviewedIds = new Set(scan.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n const lines: string[] = [];\n for (const r of scan.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of scan.formatErrors) lines.push(` ⛔ ${e}`);\n if (owed.length === 0) {\n lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');\n return lines.join('\\n') + '\\n';\n }\n lines.push('',\n `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`,\n 'work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '');\n for (const b of owed) {\n lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);\n lines.push(` subagent_type: ${b.subagent}`);\n lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');\n lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);\n lines.push('');\n }\n return lines.join('\\n');\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, reviewJsonSchemaHint, writeTemplate, PrGateConfig, RepoRootFinder,\n ChecklistInstructionsService, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffManifest, DiffManifestEntry, DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly checklistNotice: ChecklistNotice,\n private readonly instructions: ChecklistInstructionsService,\n private readonly materializer: DiffMaterializer,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false));\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (pre-review)',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Re-point pr-context.json at the extracted diff now that there is one — the scan wrote it before\n // materialization, so without this the reviewers' own context file would not mention the diff dir.\n this.rewritePrContext(repoRoot, featureName, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n this.printDiffSummary(manifest, diffDir);\n return briefings;\n }\n\n // pr-context.json is written by the scan (before the diff exists). Patch in the dir rather than\n // re-deriving the whole context: re-deriving means a second basis resolution, which is the drift.\n private rewritePrContext(repoRoot: string, featureName: string, diffDir: string): void {\n const p = path.join(repoRoot, '.webpieces', 'pr-review', featureName, 'pr-context.json');\n if (!fs.existsSync(p)) return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context is left as-is, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, one key added\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n raw['diffDir'] = diffDir;\n fs.writeFileSync(p, JSON.stringify(raw, null, 2) + '\\n');\n } catch (err: unknown) {\n const error = toError(err);\n void error; // leave the file as-is — the instructions files carry the same paths directly\n }\n }\n\n private printDiffSummary(manifest: DiffManifest, diffDir: string): void {\n process.stdout.write('\\n' + SEP + '② Diff extracted for the reviewers\\n' + SEP + '\\n');\n process.stdout.write(` ${manifest.entries.length} file(s) → ${diffDir}\\n`);\n if (manifest.excluded.length > 0) {\n process.stdout.write(` ${manifest.excluded.length} excluded by pr-gate.reviewDiffExclude (stubbed, still matched)\\n`);\n }\n const truncated = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated).length;\n if (truncated > 0) process.stdout.write(` ${truncated} truncated at the per-file cap (each says so in its footer)\\n`);\n }\n\n /** What the AI must do next: spawn each reviewer, then write review.json. */\n private report(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): void {\n process.stdout.write('\\n' + SEP + '③ Review, then finish\\n' + SEP + '\\n');\n if (scan.applicable.length === 0) {\n process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));\n } else {\n process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));\n }\n process.stdout.write(\n `\\nThen review your own changes and\\n${reviewJsonSchemaHint(scan.reviewPath)}\\n\\n` +\n `Finally run: pnpm wp-finish-upsert-pr\\n` +\n `(The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n`);\n }\n\n /**\n * One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the\n * generated instructions file is the contract, so anything restated here is a second copy that can go\n * stale — which is exactly how a removed `success` field outlived its own removal in print.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private spawnBlocks(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): string {\n const reviewedIds = new Set(scan.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n const lines: string[] = [];\n for (const r of scan.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of scan.formatErrors) lines.push(` ⛔ ${e}`);\n if (owed.length === 0) {\n lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');\n return lines.join('\\n') + '\\n';\n }\n lines.push('',\n `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`,\n 'work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '');\n for (const b of owed) {\n lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);\n lines.push(` subagent_type: ${b.subagent}`);\n lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');\n lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);\n lines.push('');\n }\n return lines.join('\\n');\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BuildAffected } from './build-affected';
|
|
2
|
+
import { GeneratedArtifactRegistry, GeneratedArtifacts } from './generated-artifact-registry';
|
|
3
|
+
import { GitExec } from './git-exec';
|
|
4
|
+
/**
|
|
5
|
+
* One path `git status --porcelain` reported as NOT satisfying "committed OR staged", with the two
|
|
6
|
+
* porcelain status columns kept so a message can say why. Data-only (per CLAUDE.md).
|
|
7
|
+
*/
|
|
8
|
+
export declare class DirtyPath {
|
|
9
|
+
path: string;
|
|
10
|
+
indexStatus: string;
|
|
11
|
+
worktreeStatus: string;
|
|
12
|
+
constructor(dirtyPath: string, indexStatus: string, worktreeStatus: string);
|
|
13
|
+
/** `?? path` — untracked, i.e. the build created a brand-new file nobody has ever committed. */
|
|
14
|
+
isUntracked(): boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What the repo-wide post-build tree check concluded, split into the ticket's two verdicts. Both lists
|
|
18
|
+
* can be non-empty at once, and when they are BOTH are reported — a stray artifact does not hide a
|
|
19
|
+
* stale design file or vice versa.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BuildArtifactVerdict {
|
|
22
|
+
/** Verdict 1 — dirty, but the build is SUPPOSED to write it: you did not commit the regeneration. */
|
|
23
|
+
staleGenerated: DirtyPath[];
|
|
24
|
+
/** Verdict 2 — dirty and nobody declared it: the build is emitting uncommitted git artifacts. */
|
|
25
|
+
strayArtifacts: DirtyPath[];
|
|
26
|
+
constructor(staleGenerated: DirtyPath[], strayArtifacts: DirtyPath[]);
|
|
27
|
+
isClean(): boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The repo-wide "did the build leave anything uncommitted?" gate, run in `wp-review-upsert-pr` right
|
|
31
|
+
* after `buildCommand`.
|
|
32
|
+
*
|
|
33
|
+
* It REPLACES the per-project `validate-di-graph-unchanged` nx target, which asked the same question in
|
|
34
|
+
* the wrong place. That target used `git status --porcelain` as its oracle while itself dirtying the
|
|
35
|
+
* tree, so nx (which hashes by file CONTENTS) saw one hash produce both a pass and a fail and labelled
|
|
36
|
+
* the task "flaky" — it was not flaky, it was stateful in a dimension nx cannot see. It also could not
|
|
37
|
+
* pass mid-merge, where `merge-in-progress-guard` blocks the `git commit` it demanded.
|
|
38
|
+
*
|
|
39
|
+
* THE PREDICATE IS "committed OR staged", and that is the whole trick:
|
|
40
|
+
*
|
|
41
|
+
* porcelain column 1 = index state, column 2 = working-tree state.
|
|
42
|
+
* Satisfied ⇔ column 2 is ' ' AND the entry is not `??`.
|
|
43
|
+
*
|
|
44
|
+
* A normal branch commits its regenerated design files → clean → passes. Mid-merge `git commit` is
|
|
45
|
+
* blocked but `git add` is expected (and the gate itself commits the resolution) → staged → passes.
|
|
46
|
+
* ONE rule covering both, with no merge special-case and no skip, because a special case is a second
|
|
47
|
+
* rule and two rules for one invariant is how they drift.
|
|
48
|
+
*
|
|
49
|
+
* Coverage is strictly larger than the target it replaces: that one only ever looked at `design.*` in
|
|
50
|
+
* one project, this looks at the WHOLE repo, so a build that quietly writes anywhere else is caught for
|
|
51
|
+
* the first time.
|
|
52
|
+
*/
|
|
53
|
+
export declare class BuildArtifactGate {
|
|
54
|
+
private readonly gitExec;
|
|
55
|
+
private readonly registry;
|
|
56
|
+
private readonly buildAffected;
|
|
57
|
+
constructor(gitExec: GitExec, registry: GeneratedArtifactRegistry, buildAffected: BuildAffected);
|
|
58
|
+
/**
|
|
59
|
+
* Run the check and throw CliExitError when the build left the tree dirty. Called immediately after
|
|
60
|
+
* the build gate passes, while the tree still holds exactly what the build produced.
|
|
61
|
+
*/
|
|
62
|
+
assertBuildLeftNothingUncommitted(repoRoot: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Parse `git status --porcelain` and sort every UNSATISFIED entry into the two verdicts. Pure, so
|
|
65
|
+
* the specs drive it with literal porcelain text instead of building repos for every case.
|
|
66
|
+
*/
|
|
67
|
+
classify(porcelain: string, artifacts: GeneratedArtifacts): BuildArtifactVerdict;
|
|
68
|
+
/**
|
|
69
|
+
* One porcelain line → a DirtyPath, or null when the line is blank or already satisfied.
|
|
70
|
+
*
|
|
71
|
+
* Format is `XY <path>`, with `XY` = index+worktree status and `R old -> new` for renames (the NEW
|
|
72
|
+
* path is the one on disk, so that is the one classified). Untracked is `?? <path>`.
|
|
73
|
+
*/
|
|
74
|
+
private parseLine;
|
|
75
|
+
/** The full failure message — verdict 1 section, verdict 2 section, or both. */
|
|
76
|
+
private render;
|
|
77
|
+
private renderStale;
|
|
78
|
+
private renderStray;
|
|
79
|
+
private listing;
|
|
80
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BuildArtifactGate = exports.BuildArtifactVerdict = exports.DirtyPath = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const build_affected_1 = require("./build-affected");
|
|
8
|
+
const generated_artifact_registry_1 = require("./generated-artifact-registry");
|
|
9
|
+
const git_exec_1 = require("./git-exec");
|
|
10
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
11
|
+
/**
|
|
12
|
+
* One path `git status --porcelain` reported as NOT satisfying "committed OR staged", with the two
|
|
13
|
+
* porcelain status columns kept so a message can say why. Data-only (per CLAUDE.md).
|
|
14
|
+
*/
|
|
15
|
+
class DirtyPath {
|
|
16
|
+
path;
|
|
17
|
+
indexStatus; // porcelain column 1 — the INDEX (staged) state
|
|
18
|
+
worktreeStatus; // porcelain column 2 — the WORKING TREE state
|
|
19
|
+
constructor(dirtyPath, indexStatus, worktreeStatus) {
|
|
20
|
+
this.path = dirtyPath;
|
|
21
|
+
this.indexStatus = indexStatus;
|
|
22
|
+
this.worktreeStatus = worktreeStatus;
|
|
23
|
+
}
|
|
24
|
+
/** `?? path` — untracked, i.e. the build created a brand-new file nobody has ever committed. */
|
|
25
|
+
isUntracked() {
|
|
26
|
+
return this.indexStatus === '?';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.DirtyPath = DirtyPath;
|
|
30
|
+
/**
|
|
31
|
+
* What the repo-wide post-build tree check concluded, split into the ticket's two verdicts. Both lists
|
|
32
|
+
* can be non-empty at once, and when they are BOTH are reported — a stray artifact does not hide a
|
|
33
|
+
* stale design file or vice versa.
|
|
34
|
+
*/
|
|
35
|
+
class BuildArtifactVerdict {
|
|
36
|
+
/** Verdict 1 — dirty, but the build is SUPPOSED to write it: you did not commit the regeneration. */
|
|
37
|
+
staleGenerated;
|
|
38
|
+
/** Verdict 2 — dirty and nobody declared it: the build is emitting uncommitted git artifacts. */
|
|
39
|
+
strayArtifacts;
|
|
40
|
+
constructor(staleGenerated, strayArtifacts) {
|
|
41
|
+
this.staleGenerated = staleGenerated;
|
|
42
|
+
this.strayArtifacts = strayArtifacts;
|
|
43
|
+
}
|
|
44
|
+
isClean() {
|
|
45
|
+
return this.staleGenerated.length === 0 && this.strayArtifacts.length === 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.BuildArtifactVerdict = BuildArtifactVerdict;
|
|
49
|
+
/**
|
|
50
|
+
* The repo-wide "did the build leave anything uncommitted?" gate, run in `wp-review-upsert-pr` right
|
|
51
|
+
* after `buildCommand`.
|
|
52
|
+
*
|
|
53
|
+
* It REPLACES the per-project `validate-di-graph-unchanged` nx target, which asked the same question in
|
|
54
|
+
* the wrong place. That target used `git status --porcelain` as its oracle while itself dirtying the
|
|
55
|
+
* tree, so nx (which hashes by file CONTENTS) saw one hash produce both a pass and a fail and labelled
|
|
56
|
+
* the task "flaky" — it was not flaky, it was stateful in a dimension nx cannot see. It also could not
|
|
57
|
+
* pass mid-merge, where `merge-in-progress-guard` blocks the `git commit` it demanded.
|
|
58
|
+
*
|
|
59
|
+
* THE PREDICATE IS "committed OR staged", and that is the whole trick:
|
|
60
|
+
*
|
|
61
|
+
* porcelain column 1 = index state, column 2 = working-tree state.
|
|
62
|
+
* Satisfied ⇔ column 2 is ' ' AND the entry is not `??`.
|
|
63
|
+
*
|
|
64
|
+
* A normal branch commits its regenerated design files → clean → passes. Mid-merge `git commit` is
|
|
65
|
+
* blocked but `git add` is expected (and the gate itself commits the resolution) → staged → passes.
|
|
66
|
+
* ONE rule covering both, with no merge special-case and no skip, because a special case is a second
|
|
67
|
+
* rule and two rules for one invariant is how they drift.
|
|
68
|
+
*
|
|
69
|
+
* Coverage is strictly larger than the target it replaces: that one only ever looked at `design.*` in
|
|
70
|
+
* one project, this looks at the WHOLE repo, so a build that quietly writes anywhere else is caught for
|
|
71
|
+
* the first time.
|
|
72
|
+
*/
|
|
73
|
+
let BuildArtifactGate = class BuildArtifactGate {
|
|
74
|
+
gitExec;
|
|
75
|
+
registry;
|
|
76
|
+
buildAffected;
|
|
77
|
+
constructor(gitExec, registry, buildAffected) {
|
|
78
|
+
this.gitExec = gitExec;
|
|
79
|
+
this.registry = registry;
|
|
80
|
+
this.buildAffected = buildAffected;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run the check and throw CliExitError when the build left the tree dirty. Called immediately after
|
|
84
|
+
* the build gate passes, while the tree still holds exactly what the build produced.
|
|
85
|
+
*/
|
|
86
|
+
assertBuildLeftNothingUncommitted(repoRoot) {
|
|
87
|
+
const artifacts = this.registry.resolve(repoRoot);
|
|
88
|
+
// porcelainStatus, NOT uncommittedFiles: the latter trims, which eats the leading space that IS
|
|
89
|
+
// the index column, turning every unstaged " M path" into a staged-looking "M path".
|
|
90
|
+
const verdict = this.classify(this.gitExec.porcelainStatus(repoRoot), artifacts);
|
|
91
|
+
if (verdict.isClean()) {
|
|
92
|
+
process.stdout.write('\n✅ Build left the tree committed — no uncommitted build artifacts.\n');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw new rules_config_1.CliExitError(1, this.render(repoRoot, verdict, artifacts));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse `git status --porcelain` and sort every UNSATISFIED entry into the two verdicts. Pure, so
|
|
99
|
+
* the specs drive it with literal porcelain text instead of building repos for every case.
|
|
100
|
+
*/
|
|
101
|
+
classify(porcelain, artifacts) {
|
|
102
|
+
const stale = [];
|
|
103
|
+
const stray = [];
|
|
104
|
+
for (const line of porcelain.split('\n')) {
|
|
105
|
+
const entry = this.parseLine(line);
|
|
106
|
+
if (entry === null)
|
|
107
|
+
continue;
|
|
108
|
+
if ((0, rules_config_1.matchesAnyGlob)(entry.path, artifacts.paths))
|
|
109
|
+
stale.push(entry);
|
|
110
|
+
else
|
|
111
|
+
stray.push(entry);
|
|
112
|
+
}
|
|
113
|
+
return new BuildArtifactVerdict(stale, stray);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* One porcelain line → a DirtyPath, or null when the line is blank or already satisfied.
|
|
117
|
+
*
|
|
118
|
+
* Format is `XY <path>`, with `XY` = index+worktree status and `R old -> new` for renames (the NEW
|
|
119
|
+
* path is the one on disk, so that is the one classified). Untracked is `?? <path>`.
|
|
120
|
+
*/
|
|
121
|
+
parseLine(line) {
|
|
122
|
+
if (line.length < 4)
|
|
123
|
+
return null;
|
|
124
|
+
const indexStatus = line.charAt(0);
|
|
125
|
+
const worktreeStatus = line.charAt(1);
|
|
126
|
+
// THE PREDICATE. A clean worktree column means the change is committed or staged — satisfied.
|
|
127
|
+
// `??` is the one entry whose column 2 is not a worktree state, so it is excluded explicitly.
|
|
128
|
+
if (worktreeStatus === ' ' && indexStatus !== '?')
|
|
129
|
+
return null;
|
|
130
|
+
const raw = line.slice(3).trim();
|
|
131
|
+
const arrow = raw.indexOf(' -> ');
|
|
132
|
+
const filePath = arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
133
|
+
const unquoted = filePath.startsWith('"') && filePath.endsWith('"') ? filePath.slice(1, -1) : filePath;
|
|
134
|
+
return unquoted === '' ? null : new DirtyPath(unquoted, indexStatus, worktreeStatus);
|
|
135
|
+
}
|
|
136
|
+
/** The full failure message — verdict 1 section, verdict 2 section, or both. */
|
|
137
|
+
render(repoRoot, verdict, artifacts) {
|
|
138
|
+
const buildCommand = this.buildAffected.resolveBuildCommand(repoRoot);
|
|
139
|
+
const parts = ['\n' + SEP + '❌ The build left uncommitted changes in the working tree\n' + SEP];
|
|
140
|
+
if (verdict.staleGenerated.length > 0)
|
|
141
|
+
parts.push(this.renderStale(verdict, buildCommand));
|
|
142
|
+
if (verdict.strayArtifacts.length > 0)
|
|
143
|
+
parts.push(this.renderStray(verdict));
|
|
144
|
+
parts.push(`\nThe rule is one line: after the build, every changed path must be COMMITTED **or** STAGED.\n` +
|
|
145
|
+
`Staged counts, which is why this also works mid-3-point-merge, where \`git commit\` is blocked\n` +
|
|
146
|
+
`by merge-in-progress-guard but \`git add\` is exactly what you are supposed to do.\n\n` +
|
|
147
|
+
`Known-generated paths came from: ${artifacts.source}\n` + SEP);
|
|
148
|
+
return parts.join('');
|
|
149
|
+
}
|
|
150
|
+
renderStale(verdict, buildCommand) {
|
|
151
|
+
return `\nYou did not run \`${buildCommand}\` and commit the regenerated design files, so the\n` +
|
|
152
|
+
`review cannot proceed. These are declared build outputs and the build just rewrote them:\n\n` +
|
|
153
|
+
this.listing(verdict.staleGenerated) +
|
|
154
|
+
`\nFix it by running the build and committing (or staging) the result:\n\n` +
|
|
155
|
+
` ${buildCommand}\n` +
|
|
156
|
+
` git add -A && git commit -m "regenerate design files"\n`;
|
|
157
|
+
}
|
|
158
|
+
renderStray(verdict) {
|
|
159
|
+
return `\nYour build is generating uncommitted git artifacts, so I cannot continue the PR process.\n` +
|
|
160
|
+
`Either (1, best) write them to an output directory that is in \`.gitignore\`, or (2) add a new\n` +
|
|
161
|
+
`dir to \`.gitignore\`.\n\n` +
|
|
162
|
+
`These paths are NOT declared as the output of any build target:\n\n` +
|
|
163
|
+
this.listing(verdict.strayArtifacts);
|
|
164
|
+
}
|
|
165
|
+
listing(entries) {
|
|
166
|
+
return entries
|
|
167
|
+
.map((e) => ` ${e.indexStatus}${e.worktreeStatus} ${e.path}${e.isUntracked() ? ' (untracked — brand new)' : ''}\n`)
|
|
168
|
+
.join('');
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
exports.BuildArtifactGate = BuildArtifactGate;
|
|
172
|
+
exports.BuildArtifactGate = BuildArtifactGate = tslib_1.__decorate([
|
|
173
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
174
|
+
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec,
|
|
175
|
+
generated_artifact_registry_1.GeneratedArtifactRegistry,
|
|
176
|
+
build_affected_1.BuildAffected])
|
|
177
|
+
], BuildArtifactGate);
|
|
178
|
+
//# sourceMappingURL=build-artifact-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-artifact-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-artifact-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, matchesAnyGlob } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildAffected } from './build-affected';\nimport { GeneratedArtifactRegistry, GeneratedArtifacts } from './generated-artifact-registry';\nimport { GitExec } from './git-exec';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One path `git status --porcelain` reported as NOT satisfying \"committed OR staged\", with the two\n * porcelain status columns kept so a message can say why. Data-only (per CLAUDE.md).\n */\nexport class DirtyPath {\n path: string;\n indexStatus: string; // porcelain column 1 — the INDEX (staged) state\n worktreeStatus: string; // porcelain column 2 — the WORKING TREE state\n\n constructor(dirtyPath: string, indexStatus: string, worktreeStatus: string) {\n this.path = dirtyPath;\n this.indexStatus = indexStatus;\n this.worktreeStatus = worktreeStatus;\n }\n\n /** `?? path` — untracked, i.e. the build created a brand-new file nobody has ever committed. */\n isUntracked(): boolean {\n return this.indexStatus === '?';\n }\n}\n\n/**\n * What the repo-wide post-build tree check concluded, split into the ticket's two verdicts. Both lists\n * can be non-empty at once, and when they are BOTH are reported — a stray artifact does not hide a\n * stale design file or vice versa.\n */\nexport class BuildArtifactVerdict {\n /** Verdict 1 — dirty, but the build is SUPPOSED to write it: you did not commit the regeneration. */\n staleGenerated: DirtyPath[];\n /** Verdict 2 — dirty and nobody declared it: the build is emitting uncommitted git artifacts. */\n strayArtifacts: DirtyPath[];\n\n constructor(staleGenerated: DirtyPath[], strayArtifacts: DirtyPath[]) {\n this.staleGenerated = staleGenerated;\n this.strayArtifacts = strayArtifacts;\n }\n\n isClean(): boolean {\n return this.staleGenerated.length === 0 && this.strayArtifacts.length === 0;\n }\n}\n\n/**\n * The repo-wide \"did the build leave anything uncommitted?\" gate, run in `wp-review-upsert-pr` right\n * after `buildCommand`.\n *\n * It REPLACES the per-project `validate-di-graph-unchanged` nx target, which asked the same question in\n * the wrong place. That target used `git status --porcelain` as its oracle while itself dirtying the\n * tree, so nx (which hashes by file CONTENTS) saw one hash produce both a pass and a fail and labelled\n * the task \"flaky\" — it was not flaky, it was stateful in a dimension nx cannot see. It also could not\n * pass mid-merge, where `merge-in-progress-guard` blocks the `git commit` it demanded.\n *\n * THE PREDICATE IS \"committed OR staged\", and that is the whole trick:\n *\n * porcelain column 1 = index state, column 2 = working-tree state.\n * Satisfied ⇔ column 2 is ' ' AND the entry is not `??`.\n *\n * A normal branch commits its regenerated design files → clean → passes. Mid-merge `git commit` is\n * blocked but `git add` is expected (and the gate itself commits the resolution) → staged → passes.\n * ONE rule covering both, with no merge special-case and no skip, because a special case is a second\n * rule and two rules for one invariant is how they drift.\n *\n * Coverage is strictly larger than the target it replaces: that one only ever looked at `design.*` in\n * one project, this looks at the WHOLE repo, so a build that quietly writes anywhere else is caught for\n * the first time.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildArtifactGate {\n constructor(\n private readonly gitExec: GitExec,\n private readonly registry: GeneratedArtifactRegistry,\n private readonly buildAffected: BuildAffected,\n ) {}\n\n /**\n * Run the check and throw CliExitError when the build left the tree dirty. Called immediately after\n * the build gate passes, while the tree still holds exactly what the build produced.\n */\n assertBuildLeftNothingUncommitted(repoRoot: string): void {\n const artifacts = this.registry.resolve(repoRoot);\n // porcelainStatus, NOT uncommittedFiles: the latter trims, which eats the leading space that IS\n // the index column, turning every unstaged \" M path\" into a staged-looking \"M path\".\n const verdict = this.classify(this.gitExec.porcelainStatus(repoRoot), artifacts);\n if (verdict.isClean()) {\n process.stdout.write('\\n✅ Build left the tree committed — no uncommitted build artifacts.\\n');\n return;\n }\n throw new CliExitError(1, this.render(repoRoot, verdict, artifacts));\n }\n\n /**\n * Parse `git status --porcelain` and sort every UNSATISFIED entry into the two verdicts. Pure, so\n * the specs drive it with literal porcelain text instead of building repos for every case.\n */\n classify(porcelain: string, artifacts: GeneratedArtifacts): BuildArtifactVerdict {\n const stale: DirtyPath[] = [];\n const stray: DirtyPath[] = [];\n for (const line of porcelain.split('\\n')) {\n const entry = this.parseLine(line);\n if (entry === null) continue;\n if (matchesAnyGlob(entry.path, artifacts.paths)) stale.push(entry);\n else stray.push(entry);\n }\n return new BuildArtifactVerdict(stale, stray);\n }\n\n /**\n * One porcelain line → a DirtyPath, or null when the line is blank or already satisfied.\n *\n * Format is `XY <path>`, with `XY` = index+worktree status and `R old -> new` for renames (the NEW\n * path is the one on disk, so that is the one classified). Untracked is `?? <path>`.\n */\n private parseLine(line: string): DirtyPath | null {\n if (line.length < 4) return null;\n const indexStatus = line.charAt(0);\n const worktreeStatus = line.charAt(1);\n // THE PREDICATE. A clean worktree column means the change is committed or staged — satisfied.\n // `??` is the one entry whose column 2 is not a worktree state, so it is excluded explicitly.\n if (worktreeStatus === ' ' && indexStatus !== '?') return null;\n const raw = line.slice(3).trim();\n const arrow = raw.indexOf(' -> ');\n const filePath = arrow >= 0 ? raw.slice(arrow + 4) : raw;\n const unquoted = filePath.startsWith('\"') && filePath.endsWith('\"') ? filePath.slice(1, -1) : filePath;\n return unquoted === '' ? null : new DirtyPath(unquoted, indexStatus, worktreeStatus);\n }\n\n /** The full failure message — verdict 1 section, verdict 2 section, or both. */\n private render(repoRoot: string, verdict: BuildArtifactVerdict, artifacts: GeneratedArtifacts): string {\n const buildCommand = this.buildAffected.resolveBuildCommand(repoRoot);\n const parts: string[] = ['\\n' + SEP + '❌ The build left uncommitted changes in the working tree\\n' + SEP];\n if (verdict.staleGenerated.length > 0) parts.push(this.renderStale(verdict, buildCommand));\n if (verdict.strayArtifacts.length > 0) parts.push(this.renderStray(verdict));\n parts.push(\n `\\nThe rule is one line: after the build, every changed path must be COMMITTED **or** STAGED.\\n` +\n `Staged counts, which is why this also works mid-3-point-merge, where \\`git commit\\` is blocked\\n` +\n `by merge-in-progress-guard but \\`git add\\` is exactly what you are supposed to do.\\n\\n` +\n `Known-generated paths came from: ${artifacts.source}\\n` + SEP);\n return parts.join('');\n }\n\n private renderStale(verdict: BuildArtifactVerdict, buildCommand: string): string {\n return `\\nYou did not run \\`${buildCommand}\\` and commit the regenerated design files, so the\\n` +\n `review cannot proceed. These are declared build outputs and the build just rewrote them:\\n\\n` +\n this.listing(verdict.staleGenerated) +\n `\\nFix it by running the build and committing (or staging) the result:\\n\\n` +\n ` ${buildCommand}\\n` +\n ` git add -A && git commit -m \"regenerate design files\"\\n`;\n }\n\n private renderStray(verdict: BuildArtifactVerdict): string {\n return `\\nYour build is generating uncommitted git artifacts, so I cannot continue the PR process.\\n` +\n `Either (1, best) write them to an output directory that is in \\`.gitignore\\`, or (2) add a new\\n` +\n `dir to \\`.gitignore\\`.\\n\\n` +\n `These paths are NOT declared as the output of any build target:\\n\\n` +\n this.listing(verdict.strayArtifacts);\n }\n\n private listing(entries: readonly DirtyPath[]): string {\n return entries\n .map((e: DirtyPath): string => ` ${e.indexStatus}${e.worktreeStatus} ${e.path}${e.isUntracked() ? ' (untracked — brand new)' : ''}\\n`)\n .join('');\n }\n}\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the "known generated" answer came from, so a failure message can say whether it is speaking
|
|
3
|
+
* from the workspace's own declarations or from the built-in fallback.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ARTIFACT_SOURCE_NX = "nx project graph (target `outputs`)";
|
|
6
|
+
export declare const ARTIFACT_SOURCE_FALLBACK = "built-in fallback (nx project graph unavailable)";
|
|
7
|
+
/**
|
|
8
|
+
* The fallback "known generated" set, used ONLY when the nx project graph cannot be read (nx not
|
|
9
|
+
* installed, `nx graph` failed, unparseable output). It deliberately mirrors what the webpieces nx
|
|
10
|
+
* plugin declares as `outputs` today — `di-graph-generate` writes design.{json,md,html} per project
|
|
11
|
+
* and the architecture targets write architecture/*.
|
|
12
|
+
*
|
|
13
|
+
* It is a fallback and not the primary source precisely because a hand-maintained list drifts: the
|
|
14
|
+
* moment a repo adds a target that writes a new committed artifact, only the nx `outputs` declaration
|
|
15
|
+
* knows about it. Data (a module-level table), not a list buried inside a function.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FALLBACK_GENERATED_PATHS: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* The set of repo-relative paths/globs that a build is EXPECTED to rewrite, plus where the set came
|
|
20
|
+
* from. Data-only (per CLAUDE.md) — the resolution logic lives in GeneratedArtifactRegistry.
|
|
21
|
+
*/
|
|
22
|
+
export declare class GeneratedArtifacts {
|
|
23
|
+
paths: string[];
|
|
24
|
+
source: string;
|
|
25
|
+
constructor(paths: string[], source: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Answers "is this path something the build is SUPPOSED to rewrite?" from the workspace's own nx
|
|
29
|
+
* target `outputs` declarations rather than from a parallel list maintained by hand.
|
|
30
|
+
*
|
|
31
|
+
* Why nx `outputs`: every target that writes a checked-in artifact already has to declare it there or
|
|
32
|
+
* nx caching corrupts the workspace, so the declaration is load-bearing and therefore maintained. A
|
|
33
|
+
* second list in the PR gate would be maintained by nobody and would drift the first time a repo adds
|
|
34
|
+
* a generator. One `nx graph --file=<tmp>` dumps the whole workspace (every project, every target,
|
|
35
|
+
* every `outputs` entry) in one ~2s call, so this costs one subprocess per PR-gate run.
|
|
36
|
+
*
|
|
37
|
+
* Tokens: `{projectRoot}` and `{workspaceRoot}` are expanded; anything still holding a `{...}` token
|
|
38
|
+
* (`{options.outputPath}`, `{options.outputFile}`) is DROPPED — it is only resolvable per-invocation,
|
|
39
|
+
* and such targets write into `dist/`, which is gitignored and therefore invisible to this gate anyway.
|
|
40
|
+
*/
|
|
41
|
+
export declare class GeneratedArtifactRegistry {
|
|
42
|
+
private cached;
|
|
43
|
+
/** The declared build outputs for this repo, from nx when readable and the fallback table otherwise. */
|
|
44
|
+
resolve(repoRoot: string): GeneratedArtifacts;
|
|
45
|
+
/** Test seam: pre-seed the resolved set so specs never shell out to nx. */
|
|
46
|
+
seed(artifacts: GeneratedArtifacts): void;
|
|
47
|
+
/**
|
|
48
|
+
* Dump the nx project graph to a temp file and collect every target's `outputs`. Returns null (not
|
|
49
|
+
* an empty list) when nx is unavailable or unreadable, so the caller can tell "no nx" apart from
|
|
50
|
+
* "nx says this workspace declares nothing".
|
|
51
|
+
*/
|
|
52
|
+
private readNxOutputs;
|
|
53
|
+
/** Read + narrow the dumped graph. Null on anything unreadable, so the caller falls back. */
|
|
54
|
+
private parseGraph;
|
|
55
|
+
/** Walk every project → every target → every `outputs` entry, expanding the nx path tokens. */
|
|
56
|
+
private collectOutputs;
|
|
57
|
+
/** Expand `{projectRoot}` / `{workspaceRoot}`; drop anything still carrying an unresolved token. */
|
|
58
|
+
private expand;
|
|
59
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GeneratedArtifactRegistry = exports.GeneratedArtifacts = exports.FALLBACK_GENERATED_PATHS = exports.ARTIFACT_SOURCE_FALLBACK = exports.ARTIFACT_SOURCE_NX = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const os = tslib_1.__importStar(require("os"));
|
|
8
|
+
const path = tslib_1.__importStar(require("path"));
|
|
9
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
10
|
+
const inversify_1 = require("inversify");
|
|
11
|
+
/**
|
|
12
|
+
* Where the "known generated" answer came from, so a failure message can say whether it is speaking
|
|
13
|
+
* from the workspace's own declarations or from the built-in fallback.
|
|
14
|
+
*/
|
|
15
|
+
exports.ARTIFACT_SOURCE_NX = 'nx project graph (target `outputs`)';
|
|
16
|
+
exports.ARTIFACT_SOURCE_FALLBACK = 'built-in fallback (nx project graph unavailable)';
|
|
17
|
+
/**
|
|
18
|
+
* The fallback "known generated" set, used ONLY when the nx project graph cannot be read (nx not
|
|
19
|
+
* installed, `nx graph` failed, unparseable output). It deliberately mirrors what the webpieces nx
|
|
20
|
+
* plugin declares as `outputs` today — `di-graph-generate` writes design.{json,md,html} per project
|
|
21
|
+
* and the architecture targets write architecture/*.
|
|
22
|
+
*
|
|
23
|
+
* It is a fallback and not the primary source precisely because a hand-maintained list drifts: the
|
|
24
|
+
* moment a repo adds a target that writes a new committed artifact, only the nx `outputs` declaration
|
|
25
|
+
* knows about it. Data (a module-level table), not a list buried inside a function.
|
|
26
|
+
*/
|
|
27
|
+
exports.FALLBACK_GENERATED_PATHS = [
|
|
28
|
+
'**/design.json',
|
|
29
|
+
'**/design.md',
|
|
30
|
+
'**/design.html',
|
|
31
|
+
'architecture/dependencies.json',
|
|
32
|
+
'architecture/dependencies.html',
|
|
33
|
+
'architecture/runtime-dependencies.json',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* The set of repo-relative paths/globs that a build is EXPECTED to rewrite, plus where the set came
|
|
37
|
+
* from. Data-only (per CLAUDE.md) — the resolution logic lives in GeneratedArtifactRegistry.
|
|
38
|
+
*/
|
|
39
|
+
class GeneratedArtifacts {
|
|
40
|
+
paths;
|
|
41
|
+
source;
|
|
42
|
+
constructor(paths, source) {
|
|
43
|
+
this.paths = paths;
|
|
44
|
+
this.source = source;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.GeneratedArtifacts = GeneratedArtifacts;
|
|
48
|
+
/**
|
|
49
|
+
* Answers "is this path something the build is SUPPOSED to rewrite?" from the workspace's own nx
|
|
50
|
+
* target `outputs` declarations rather than from a parallel list maintained by hand.
|
|
51
|
+
*
|
|
52
|
+
* Why nx `outputs`: every target that writes a checked-in artifact already has to declare it there or
|
|
53
|
+
* nx caching corrupts the workspace, so the declaration is load-bearing and therefore maintained. A
|
|
54
|
+
* second list in the PR gate would be maintained by nobody and would drift the first time a repo adds
|
|
55
|
+
* a generator. One `nx graph --file=<tmp>` dumps the whole workspace (every project, every target,
|
|
56
|
+
* every `outputs` entry) in one ~2s call, so this costs one subprocess per PR-gate run.
|
|
57
|
+
*
|
|
58
|
+
* Tokens: `{projectRoot}` and `{workspaceRoot}` are expanded; anything still holding a `{...}` token
|
|
59
|
+
* (`{options.outputPath}`, `{options.outputFile}`) is DROPPED — it is only resolvable per-invocation,
|
|
60
|
+
* and such targets write into `dist/`, which is gitignored and therefore invisible to this gate anyway.
|
|
61
|
+
*/
|
|
62
|
+
let GeneratedArtifactRegistry = class GeneratedArtifactRegistry {
|
|
63
|
+
// Resolved once per process: the graph does not change while one PR-gate command runs.
|
|
64
|
+
cached = null;
|
|
65
|
+
/** The declared build outputs for this repo, from nx when readable and the fallback table otherwise. */
|
|
66
|
+
resolve(repoRoot) {
|
|
67
|
+
if (this.cached !== null)
|
|
68
|
+
return this.cached;
|
|
69
|
+
const fromNx = this.readNxOutputs(repoRoot);
|
|
70
|
+
this.cached = fromNx !== null
|
|
71
|
+
? new GeneratedArtifacts(fromNx, exports.ARTIFACT_SOURCE_NX)
|
|
72
|
+
: new GeneratedArtifacts(exports.FALLBACK_GENERATED_PATHS.slice(), exports.ARTIFACT_SOURCE_FALLBACK);
|
|
73
|
+
return this.cached;
|
|
74
|
+
}
|
|
75
|
+
/** Test seam: pre-seed the resolved set so specs never shell out to nx. */
|
|
76
|
+
seed(artifacts) {
|
|
77
|
+
this.cached = artifacts;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Dump the nx project graph to a temp file and collect every target's `outputs`. Returns null (not
|
|
81
|
+
* an empty list) when nx is unavailable or unreadable, so the caller can tell "no nx" apart from
|
|
82
|
+
* "nx says this workspace declares nothing".
|
|
83
|
+
*/
|
|
84
|
+
readNxOutputs(repoRoot) {
|
|
85
|
+
const nxBin = path.join(repoRoot, 'node_modules', '.bin', 'nx');
|
|
86
|
+
if (!fs.existsSync(nxBin))
|
|
87
|
+
return null;
|
|
88
|
+
const outFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-nxgraph-')), 'graph.json');
|
|
89
|
+
const run = (0, child_process_1.spawnSync)(nxBin, ['graph', '--file', outFile], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });
|
|
90
|
+
if (run.status !== 0 || !fs.existsSync(outFile))
|
|
91
|
+
return null;
|
|
92
|
+
return this.parseGraph(outFile);
|
|
93
|
+
}
|
|
94
|
+
/** Read + narrow the dumped graph. Null on anything unreadable, so the caller falls back. */
|
|
95
|
+
parseGraph(outFile) {
|
|
96
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/corrupt graph dump must
|
|
97
|
+
// fall back to the built-in table, never fail a PR flow over a cache artifact we only consult
|
|
98
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
99
|
+
try {
|
|
100
|
+
// webpieces-disable no-any-unknown -- opaque nx JSON, narrowed field by field below
|
|
101
|
+
const parsed = JSON.parse(fs.readFileSync(outFile, 'utf8'));
|
|
102
|
+
const nodes = parsed.graph?.nodes;
|
|
103
|
+
if (nodes === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
return this.collectOutputs(nodes);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const error = (0, rules_config_1.toError)(err);
|
|
109
|
+
void error; // unreadable graph => fall back to the built-in table
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Walk every project → every target → every `outputs` entry, expanding the nx path tokens. */
|
|
114
|
+
collectOutputs(nodes) {
|
|
115
|
+
const found = new Set();
|
|
116
|
+
for (const projectName of Object.keys(nodes)) {
|
|
117
|
+
const data = nodes[projectName].data;
|
|
118
|
+
const targets = data.targets;
|
|
119
|
+
if (targets === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
for (const targetName of Object.keys(targets)) {
|
|
122
|
+
for (const raw of targets[targetName].outputs ?? []) {
|
|
123
|
+
const expanded = this.expand(raw, data.root ?? '.');
|
|
124
|
+
if (expanded !== null)
|
|
125
|
+
found.add(expanded);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return Array.from(found).sort();
|
|
130
|
+
}
|
|
131
|
+
/** Expand `{projectRoot}` / `{workspaceRoot}`; drop anything still carrying an unresolved token. */
|
|
132
|
+
expand(raw, projectRoot) {
|
|
133
|
+
const replaced = raw
|
|
134
|
+
.replace(/\{projectRoot\}/g, projectRoot)
|
|
135
|
+
.replace(/\{workspaceRoot\}\//g, '')
|
|
136
|
+
.replace(/\{workspaceRoot\}/g, '.');
|
|
137
|
+
if (replaced.includes('{'))
|
|
138
|
+
return null;
|
|
139
|
+
const normalized = replaced.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
140
|
+
return normalized === '' || normalized === '.' ? null : normalized;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
exports.GeneratedArtifactRegistry = GeneratedArtifactRegistry;
|
|
144
|
+
exports.GeneratedArtifactRegistry = GeneratedArtifactRegistry = tslib_1.__decorate([
|
|
145
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
146
|
+
], GeneratedArtifactRegistry);
|
|
147
|
+
//# sourceMappingURL=generated-artifact-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated-artifact-registry.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/generated-artifact-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Where the \"known generated\" answer came from, so a failure message can say whether it is speaking\n * from the workspace's own declarations or from the built-in fallback.\n */\nexport const ARTIFACT_SOURCE_NX = 'nx project graph (target `outputs`)';\nexport const ARTIFACT_SOURCE_FALLBACK = 'built-in fallback (nx project graph unavailable)';\n\n/**\n * The fallback \"known generated\" set, used ONLY when the nx project graph cannot be read (nx not\n * installed, `nx graph` failed, unparseable output). It deliberately mirrors what the webpieces nx\n * plugin declares as `outputs` today — `di-graph-generate` writes design.{json,md,html} per project\n * and the architecture targets write architecture/*.\n *\n * It is a fallback and not the primary source precisely because a hand-maintained list drifts: the\n * moment a repo adds a target that writes a new committed artifact, only the nx `outputs` declaration\n * knows about it. Data (a module-level table), not a list buried inside a function.\n */\nexport const FALLBACK_GENERATED_PATHS: readonly string[] = [\n '**/design.json',\n '**/design.md',\n '**/design.html',\n 'architecture/dependencies.json',\n 'architecture/dependencies.html',\n 'architecture/runtime-dependencies.json',\n];\n\n/**\n * The set of repo-relative paths/globs that a build is EXPECTED to rewrite, plus where the set came\n * from. Data-only (per CLAUDE.md) — the resolution logic lives in GeneratedArtifactRegistry.\n */\nexport class GeneratedArtifacts {\n paths: string[];\n source: string;\n\n constructor(paths: string[], source: string) {\n this.paths = paths;\n this.source = source;\n }\n}\n\n/**\n * Answers \"is this path something the build is SUPPOSED to rewrite?\" from the workspace's own nx\n * target `outputs` declarations rather than from a parallel list maintained by hand.\n *\n * Why nx `outputs`: every target that writes a checked-in artifact already has to declare it there or\n * nx caching corrupts the workspace, so the declaration is load-bearing and therefore maintained. A\n * second list in the PR gate would be maintained by nobody and would drift the first time a repo adds\n * a generator. One `nx graph --file=<tmp>` dumps the whole workspace (every project, every target,\n * every `outputs` entry) in one ~2s call, so this costs one subprocess per PR-gate run.\n *\n * Tokens: `{projectRoot}` and `{workspaceRoot}` are expanded; anything still holding a `{...}` token\n * (`{options.outputPath}`, `{options.outputFile}`) is DROPPED — it is only resolvable per-invocation,\n * and such targets write into `dist/`, which is gitignored and therefore invisible to this gate anyway.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class GeneratedArtifactRegistry {\n // Resolved once per process: the graph does not change while one PR-gate command runs.\n private cached: GeneratedArtifacts | null = null;\n\n /** The declared build outputs for this repo, from nx when readable and the fallback table otherwise. */\n resolve(repoRoot: string): GeneratedArtifacts {\n if (this.cached !== null) return this.cached;\n const fromNx = this.readNxOutputs(repoRoot);\n this.cached = fromNx !== null\n ? new GeneratedArtifacts(fromNx, ARTIFACT_SOURCE_NX)\n : new GeneratedArtifacts(FALLBACK_GENERATED_PATHS.slice(), ARTIFACT_SOURCE_FALLBACK);\n return this.cached;\n }\n\n /** Test seam: pre-seed the resolved set so specs never shell out to nx. */\n seed(artifacts: GeneratedArtifacts): void {\n this.cached = artifacts;\n }\n\n /**\n * Dump the nx project graph to a temp file and collect every target's `outputs`. Returns null (not\n * an empty list) when nx is unavailable or unreadable, so the caller can tell \"no nx\" apart from\n * \"nx says this workspace declares nothing\".\n */\n private readNxOutputs(repoRoot: string): string[] | null {\n const nxBin = path.join(repoRoot, 'node_modules', '.bin', 'nx');\n if (!fs.existsSync(nxBin)) return null;\n const outFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-nxgraph-')), 'graph.json');\n const run = spawnSync(nxBin, ['graph', '--file', outFile], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });\n if (run.status !== 0 || !fs.existsSync(outFile)) return null;\n return this.parseGraph(outFile);\n }\n\n /** Read + narrow the dumped graph. Null on anything unreadable, so the caller falls back. */\n private parseGraph(outFile: string): string[] | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/corrupt graph dump must\n // fall back to the built-in table, never fail a PR flow over a cache artifact we only consult\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque nx JSON, narrowed field by field below\n const parsed = JSON.parse(fs.readFileSync(outFile, 'utf8')) as NxGraphFile;\n const nodes = parsed.graph?.nodes;\n if (nodes === undefined) return null;\n return this.collectOutputs(nodes);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // unreadable graph => fall back to the built-in table\n return null;\n }\n }\n\n /** Walk every project → every target → every `outputs` entry, expanding the nx path tokens. */\n private collectOutputs(nodes: Record<string, NxGraphNode>): string[] {\n const found = new Set<string>();\n for (const projectName of Object.keys(nodes)) {\n const data = nodes[projectName].data;\n const targets = data.targets;\n if (targets === undefined) continue;\n for (const targetName of Object.keys(targets)) {\n for (const raw of targets[targetName].outputs ?? []) {\n const expanded = this.expand(raw, data.root ?? '.');\n if (expanded !== null) found.add(expanded);\n }\n }\n }\n return Array.from(found).sort();\n }\n\n /** Expand `{projectRoot}` / `{workspaceRoot}`; drop anything still carrying an unresolved token. */\n private expand(raw: string, projectRoot: string): string | null {\n const replaced = raw\n .replace(/\\{projectRoot\\}/g, projectRoot)\n .replace(/\\{workspaceRoot\\}\\//g, '')\n .replace(/\\{workspaceRoot\\}/g, '.');\n if (replaced.includes('{')) return null;\n const normalized = replaced.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n return normalized === '' || normalized === '.' ? null : normalized;\n }\n}\n\n// Structural views of the `nx graph --file=<json>` payload. Interfaces (not classes) on purpose: these\n// describe a foreign JSON shape we narrow, never something this codebase constructs.\ninterface NxGraphFile {\n graph?: NxGraphBody;\n}\n\ninterface NxGraphBody {\n nodes?: Record<string, NxGraphNode>;\n}\n\ninterface NxGraphNode {\n data: NxProjectData;\n}\n\ninterface NxProjectData {\n root?: string;\n targets?: Record<string, NxTarget>;\n}\n\ninterface NxTarget {\n outputs?: string[];\n}\n"]}
|
|
@@ -4,6 +4,16 @@ export declare class GitExec {
|
|
|
4
4
|
private readonly repoRootFinder;
|
|
5
5
|
constructor(repoRootFinder: RepoRootFinder);
|
|
6
6
|
uncommittedFiles(cwd: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Raw `git status --porcelain` with ONLY the trailing newline(s) stripped.
|
|
9
|
+
*
|
|
10
|
+
* uncommittedFiles() trims, and trimming DESTROYS DATA here: porcelain column 1 is the index state
|
|
11
|
+
* and column 2 the worktree state, so an unstaged modification is " M path" — trim() eats that
|
|
12
|
+
* leading space and the line then reads as "M " (staged, clean worktree), i.e. the exact opposite.
|
|
13
|
+
* Anything that parses the two status columns (BuildArtifactGate) must use THIS method; the trimmed
|
|
14
|
+
* one is only safe for "is it empty?" and for printing.
|
|
15
|
+
*/
|
|
16
|
+
porcelainStatus(cwd: string): string;
|
|
7
17
|
untrackedFiles(cwd: string): string;
|
|
8
18
|
/**
|
|
9
19
|
* Precondition for every PR/merge entry point: the working tree must be fully committed. The
|
|
@@ -29,4 +39,5 @@ export declare class GitExec {
|
|
|
29
39
|
*/
|
|
30
40
|
ensurePushed(currentBranch: string): void;
|
|
31
41
|
private gitQuery;
|
|
42
|
+
private rawGitQuery;
|
|
32
43
|
}
|
|
@@ -20,6 +20,18 @@ let GitExec = class GitExec {
|
|
|
20
20
|
uncommittedFiles(cwd) {
|
|
21
21
|
return this.gitQuery(['status', '--porcelain'], cwd, 'Failed to run `git status --porcelain` to check the working tree.');
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Raw `git status --porcelain` with ONLY the trailing newline(s) stripped.
|
|
25
|
+
*
|
|
26
|
+
* uncommittedFiles() trims, and trimming DESTROYS DATA here: porcelain column 1 is the index state
|
|
27
|
+
* and column 2 the worktree state, so an unstaged modification is " M path" — trim() eats that
|
|
28
|
+
* leading space and the line then reads as "M " (staged, clean worktree), i.e. the exact opposite.
|
|
29
|
+
* Anything that parses the two status columns (BuildArtifactGate) must use THIS method; the trimmed
|
|
30
|
+
* one is only safe for "is it empty?" and for printing.
|
|
31
|
+
*/
|
|
32
|
+
porcelainStatus(cwd) {
|
|
33
|
+
return this.rawGitQuery(['status', '--porcelain'], cwd, 'Failed to run `git status --porcelain` to check the working tree.').replace(/\n+$/, '');
|
|
34
|
+
}
|
|
23
35
|
// Untracked files only (respects .gitignore). Empty string = none.
|
|
24
36
|
untrackedFiles(cwd) {
|
|
25
37
|
return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');
|
|
@@ -88,11 +100,15 @@ let GitExec = class GitExec {
|
|
|
88
100
|
// Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit
|
|
89
101
|
// because `git ls-files --others` is scoped to the cwd subtree.
|
|
90
102
|
gitQuery(args, cwd, failMsg) {
|
|
103
|
+
return this.rawGitQuery(args, cwd, failMsg).trim();
|
|
104
|
+
}
|
|
105
|
+
// The untrimmed sibling — see porcelainStatus for why trimming is not always safe.
|
|
106
|
+
rawGitQuery(args, cwd, failMsg) {
|
|
91
107
|
const out = (0, child_process_1.spawnSync)('git', args, { encoding: 'utf8', cwd });
|
|
92
108
|
if (out.status !== 0) {
|
|
93
109
|
throw new rules_config_1.CliExitError(1, `❌ ${failMsg}`);
|
|
94
110
|
}
|
|
95
|
-
return out.stdout
|
|
111
|
+
return out.stdout;
|
|
96
112
|
}
|
|
97
113
|
};
|
|
98
114
|
exports.GitExec = GitExec;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n// The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the\n// REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).\nconst GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';\n\n/** Shared git precondition checks + push logic for the PR/merge workflow. */\n@injectable(bindingScopeValues.Singleton)\nexport class GitExec {\n constructor(private readonly repoRootFinder: RepoRootFinder) {}\n\n // Tracked changes (staged + unstaged) AND untracked files, EXCLUDING gitignored paths — so\n // `.webpieces/` tooling artifacts never count. Empty string = fully committed.\n uncommittedFiles(cwd: string): string {\n return this.gitQuery(['status', '--porcelain'], cwd, 'Failed to run `git status --porcelain` to check the working tree.');\n }\n\n /**\n * Raw `git status --porcelain` with ONLY the trailing newline(s) stripped.\n *\n * uncommittedFiles() trims, and trimming DESTROYS DATA here: porcelain column 1 is the index state\n * and column 2 the worktree state, so an unstaged modification is \" M path\" — trim() eats that\n * leading space and the line then reads as \"M \" (staged, clean worktree), i.e. the exact opposite.\n * Anything that parses the two status columns (BuildArtifactGate) must use THIS method; the trimmed\n * one is only safe for \"is it empty?\" and for printing.\n */\n porcelainStatus(cwd: string): string {\n return this.rawGitQuery(['status', '--porcelain'], cwd,\n 'Failed to run `git status --porcelain` to check the working tree.').replace(/\\n+$/, '');\n }\n\n // Untracked files only (respects .gitignore). Empty string = none.\n untrackedFiles(cwd: string): string {\n return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');\n }\n\n /**\n * Precondition for every PR/merge entry point: the working tree must be fully committed. The\n * webpieces tooling deliberately does NOT `git add`/`commit` the developer's work for them. Aborts\n * with instructions if anything is uncommitted (tracked or untracked, excluding gitignored).\n */\n assertCleanTree(cwd: string): void {\n const dirty = this.uncommittedFiles(cwd);\n if (dirty === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: You have uncommitted or untracked changes\\n' +\n SEP + '\\n' +\n 'The webpieces PR tooling will NOT commit your work for you. Commit your\\n' +\n 'changes, and either commit or delete any untracked files, then re-run.\\n\\n' +\n 'Working tree (git status --porcelain):\\n' +\n dirty + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Narrower guard for the merge-resolve commit point, where tracked resolutions are legitimately in\n * the tree but untracked files must NOT be swept into the squash commit. Lists any untracked files\n * and aborts so the AI commits or deletes them explicitly.\n */\n assertNoUntracked(cwd: string): void {\n const untracked = this.untrackedFiles(cwd);\n if (untracked === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: Untracked files present during merge finalize\\n' +\n SEP + '\\n' +\n 'The tooling will not sweep untracked files into the squash commit. Commit\\n' +\n 'or delete these, then re-run:\\n\\n' +\n untracked + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Run a git command that is expected to succeed; abort the process with a clear message if it\n * fails. Used for fetch/pull/checkout/commit where silently continuing on failure would operate on\n * stale or wrong state.\n */\n runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n throw new CliExitError(1, `❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})`);\n }\n }\n\n /**\n * Push HEAD to origin/<currentBranch>. Uses --force-with-lease for an existing remote branch (the\n * 3-point squash rewrites history) and -u for a brand-new branch.\n */\n ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n this.runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n }\n\n // Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit\n // because `git ls-files --others` is scoped to the cwd subtree.\n private gitQuery(args: string[], cwd: string, failMsg: string): string {\n return this.rawGitQuery(args, cwd, failMsg).trim();\n }\n\n // The untrimmed sibling — see porcelainStatus for why trimming is not always safe.\n private rawGitQuery(args: string[], cwd: string, failMsg: string): string {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n if (out.status !== 0) {\n throw new CliExitError(1, `❌ ${failMsg}`);\n }\n return out.stdout;\n }\n}\n"]}
|