amsd-pipeline-shared 0.5.0 → 0.5.2
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/docs/gateway-baseline-2.0.80.md5 +47 -0
- package/docs/gateway-files.txt +47 -0
- package/orchestrations/config/repo-artifacts.json +23 -0
- package/orchestrations/config/resume-preserves.json +25 -0
- package/orchestrations/config/run-modes.json +47 -0
- package/orchestrations/scripts/lib/checkpoints.sh +175 -0
- package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
- package/orchestrations/scripts/lib/codeline-packages.js +67 -0
- package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
- package/orchestrations/scripts/lib/common.sh +66 -0
- package/orchestrations/scripts/lib/control-plane.sh +506 -0
- package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
- package/orchestrations/scripts/lib/engine-paths.sh +52 -0
- package/orchestrations/scripts/lib/flags.sh +38 -0
- package/orchestrations/scripts/lib/git-ops.sh +975 -0
- package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
- package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
- package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
- package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
- package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
- package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
- package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
- package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
- package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
- package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
- package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
- package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
- package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
- package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
- package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
- package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
- package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
- package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
- package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
- package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
- package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
- package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
- package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
- package/orchestrations/scripts/lib/phase-exit.sh +90 -0
- package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
- package/orchestrations/scripts/lib/plan-hash.js +19 -0
- package/orchestrations/scripts/lib/plan-scope.js +73 -0
- package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
- package/orchestrations/scripts/lib/release-lines.sh +166 -0
- package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
- package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
- package/orchestrations/scripts/lib/run-modes.sh +93 -0
- package/orchestrations/scripts/lib/story-outputs.sh +322 -0
- package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# orchestration-resume.sh — SHOULD THIS RUN PICK UP WHERE ANOTHER LEFT OFF?
|
|
3
|
+
#
|
|
4
|
+
# Extracted verbatim from run-agent-orchestration.sh, where it sat as a top-level block among that
|
|
5
|
+
# file's function definitions and could therefore be reached only by running the whole pipeline.
|
|
6
|
+
#
|
|
7
|
+
# What it decides is not small: a resume restores a checkpoint, works out which steps to skip, and
|
|
8
|
+
# then continues the run or refuses it. Three refusals guard it, and the file prices the last one —
|
|
9
|
+
# a resume that skips the spec pass against a PRD carrying none of its output hands the writer a
|
|
10
|
+
# story with nothing to aim at, measured 2026-08-10 at $11.76 and no code.
|
|
11
|
+
#
|
|
12
|
+
# PURE CODE MOVEMENT. Assignments without `local` stay global, so ORCH_RUN_ID and the exported skip
|
|
13
|
+
# environment reach the rest of the run exactly as before, and `exit` still exits the script.
|
|
14
|
+
#
|
|
15
|
+
# Requires from its caller: is_parent, restore_run_checkpoint, list_run_checkpoints,
|
|
16
|
+
# resume_skip_env, resume_spec_output_present, error/info/success, and PRD_FILE.
|
|
17
|
+
|
|
18
|
+
# THE SPEC PASS RUNS ON A RESUME FOR THE STORIES THAT LACK IT. The run mode turns the pass off
|
|
19
|
+
# for a resume, which is right only while every pending story was elaborated; a story whose
|
|
20
|
+
# content the remediation restored from canonical (its elaboration had gone to a placeholder
|
|
21
|
+
# child) reached the writer with no spec, paid for (regintel run 20260915T101555Z, 2026-09-15).
|
|
22
|
+
# The pass is turned back on when any pending story of the phase carries no spec output; the
|
|
23
|
+
# spec runner then takes those stories and skips the elaborated ones (storiesForSpecPass).
|
|
24
|
+
resume_spec_mode_for_pending_stories() {
|
|
25
|
+
[ "${EPAM_SPEC_MODE:-1}" = "0" ] || return 0
|
|
26
|
+
[ -n "${PRD_FILE:-}" ] && [ -f "${PRD_FILE}" ] || return 0
|
|
27
|
+
local _lacking
|
|
28
|
+
_lacking=$("${NODE_BIN:-node}" "$SCRIPT_DIR/lib/handlers/stories-lacking-spec.js" "$PRD_FILE" "${PHASE:-}" 2>/dev/null | tr '\n' ' ')
|
|
29
|
+
[ -n "${_lacking// /}" ] || return 0
|
|
30
|
+
info "[orch] resume: EPAM_SPEC_MODE=1 — pending story(ies) with no spec output: ${_lacking}(the spec pass runs for these and skips the rest)"
|
|
31
|
+
export EPAM_SPEC_MODE=1
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
apply_resume_if_requested() {
|
|
36
|
+
if is_parent && [ -n "${EPAM_RESUME_RUN:-}" ]; then
|
|
37
|
+
# The roster and its briefs are stored against the run that minted them, so a resumed run
|
|
38
|
+
# must BE that run — otherwise the store reads as another run's and is not re-applied.
|
|
39
|
+
export ORCH_RUN_ID="$EPAM_RESUME_RUN"
|
|
40
|
+
|
|
41
|
+
if ! restore_run_checkpoint "$EPAM_RESUME_RUN"; then
|
|
42
|
+
error "[orch] cannot resume run '${EPAM_RESUME_RUN}' — refusing to continue against un-restored state."
|
|
43
|
+
error "[orch] available checkpoints: $(list_run_checkpoints | tr '\n' ' ' 2>/dev/null || echo none)"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if ! _resume_env=$(resume_skip_env "$EPAM_RESUME_RUN"); then
|
|
48
|
+
error "[orch] cannot determine what to skip for run '${EPAM_RESUME_RUN}' — refusing to guess."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
while IFS= read -r _assign; do
|
|
52
|
+
[ -n "$_assign" ] || continue
|
|
53
|
+
export "${_assign?}"
|
|
54
|
+
info "[orch] resume: ${_assign}"
|
|
55
|
+
done <<< "$_resume_env"
|
|
56
|
+
# Skipping the spec pass is only safe while its output still exists. restore_run_checkpoint
|
|
57
|
+
# and resume_skip_env both refuse rather than guess; this is the third case and was the
|
|
58
|
+
# silent one. Gated on the skip actually being in force — a resume that is about to RUN the
|
|
59
|
+
# spec pass has nothing to protect.
|
|
60
|
+
resume_spec_mode_for_pending_stories
|
|
61
|
+
if [ "${EPAM_SPEC_MODE:-1}" = "0" ] && ! resume_spec_output_present "$PRD_FILE"; then
|
|
62
|
+
error "[orch] resume '${EPAM_RESUME_RUN}' skips the spec pass (EPAM_SPEC_MODE=0), but the PRD"
|
|
63
|
+
error "[orch] at ${PRD_FILE} carries none of its output — no fixSiteAnalysis, no"
|
|
64
|
+
error "[orch] verificationCriteria, no declared files, no specification block."
|
|
65
|
+
error "[orch] Something overwrote the PRD after the spec pass ran. Running now would hand the"
|
|
66
|
+
error "[orch] writer a story with nothing to aim at — measured 2026-08-10 at \$11.76 and no code."
|
|
67
|
+
error "[orch] Recover with ONE of:"
|
|
68
|
+
error "[orch] - restore the PRD that carries the spec (git, or a run archive), then resume again"
|
|
69
|
+
error "[orch] - re-run the spec pass for this run: EPAM_SPEC_MODE=1 with the same EPAM_RESUME_RUN"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
success "[orch] RESUMED run ${EPAM_RESUME_RUN} — continuing from its checkpoint"
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# WHAT A PHASE'S EXIT CODE MEANS — DEFINED ONCE.
|
|
3
|
+
#
|
|
4
|
+
# run-agent-orchestration.sh exits non-zero for two completely different reasons, and
|
|
5
|
+
# until 2026-08-14 both were exit 2:
|
|
6
|
+
#
|
|
7
|
+
# REMEDIATED a gate applied a fix; re-running the phase is expected to now pass.
|
|
8
|
+
# HALT a change the reviewer never approved, at an EXHAUSTED ladder.
|
|
9
|
+
# Re-running cannot help — there is no higher rung to try.
|
|
10
|
+
#
|
|
11
|
+
# SEVEN separate call sites each wrote `if [ "$phase_exit" -eq 2 ]; then ...retry...`
|
|
12
|
+
# — run-agent-orchestration.sh (twice), orchestrate.sh, and four tier3 launchers
|
|
13
|
+
# (metrolinx, mock, skyscanner, travel-app) — so a HALT was retried by all of them.
|
|
14
|
+
# The first sweep for this found only three; the class was wider than the sites that
|
|
15
|
+
# happened to be open at the time, which is exactly why the comparison lives here now
|
|
16
|
+
# and not at any call site.
|
|
17
|
+
#
|
|
18
|
+
# WHAT THAT COST, live run 20260814T213253Z (metrolinx, AMSD-2041): Step 3.6 correctly
|
|
19
|
+
# printed "A change the reviewer never approved must NOT proceed — human review
|
|
20
|
+
# required" and exited. The caller read 2, called it remediation, and retried. The
|
|
21
|
+
# retry re-entered ensure_story_branch, which hard-reset the branch and orphaned the
|
|
22
|
+
# already-committed, already-green work; it then burned 12 further attempts against a
|
|
23
|
+
# ladder that had nothing left to escalate to, and failed. The pipeline spent ~15
|
|
24
|
+
# minutes and real money to arrive where Step 3.6 already was.
|
|
25
|
+
#
|
|
26
|
+
# The distinction is the whole point, so it lives in ONE place. A caller asks
|
|
27
|
+
# `phase_exit_is_retryable`; no caller compares a bare integer.
|
|
28
|
+
|
|
29
|
+
# Exit code vocabulary. Values are the contract with run-agent-orchestration.sh.
|
|
30
|
+
PHASE_EXIT_OK=0
|
|
31
|
+
PHASE_EXIT_REMEDIATED=2
|
|
32
|
+
PHASE_EXIT_HALT_REVIEW=3
|
|
33
|
+
PHASE_EXIT_TESTS_STRENGTHENED=4
|
|
34
|
+
|
|
35
|
+
# THE CODE WAS RIGHT; ITS TESTS WERE NOT (AMSD-2974 live L2, 2026-10-14). A gate finding that only tests can
|
|
36
|
+
# resolve — a surviving mutant in correct code — went to the AC remediator and back to the CODE writer, who is
|
|
37
|
+
# forbidden to touch tests, and looped. Such a finding now goes to the test-writer, which commits the
|
|
38
|
+
# strengthened test; the phase is re-run on that commit WITHOUT --reset, so the stories keep their approved
|
|
39
|
+
# work, Step 1 passes them as completed, and the review and the gates judge the new commit.
|
|
40
|
+
|
|
41
|
+
# phase_exit_is_retryable <code>
|
|
42
|
+
# True (0) ONLY for the code that means "a fix was applied, try again". Everything else
|
|
43
|
+
# — success, halt, and any unrecognised code — is not retryable. An unknown code is
|
|
44
|
+
# deliberately NOT retried: re-running an outcome nobody has classified is how a HALT
|
|
45
|
+
# came to be retried in the first place.
|
|
46
|
+
phase_exit_is_retryable() {
|
|
47
|
+
[ "${1:-}" = "$PHASE_EXIT_REMEDIATED" ] || [ "${1:-}" = "$PHASE_EXIT_TESTS_STRENGTHENED" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# phase_exit_retry_args <code> — the arguments a retry of this outcome re-runs the phase with, one per line.
|
|
51
|
+
# REMEDIATED changed what the stories must do, so they are reset and re-written; TESTS_STRENGTHENED changed only
|
|
52
|
+
# the tests, so the stories keep their work and the phase re-judges it.
|
|
53
|
+
phase_exit_retry_args() {
|
|
54
|
+
case "${1:-}" in
|
|
55
|
+
"$PHASE_EXIT_REMEDIATED") printf '%s\n' '--reset' ;;
|
|
56
|
+
*) ;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# phase_exit_describe <code>
|
|
61
|
+
# One line naming the outcome, so a failure reads as a decision rather than a number.
|
|
62
|
+
phase_exit_describe() {
|
|
63
|
+
case "${1:-}" in
|
|
64
|
+
"$PHASE_EXIT_OK") echo "completed" ;;
|
|
65
|
+
"$PHASE_EXIT_REMEDIATED") echo "gate remediation applied — a retry is expected to help" ;;
|
|
66
|
+
"$PHASE_EXIT_TESTS_STRENGTHENED") echo "the test-writer strengthened the tests a gate found insufficient — the gates re-run on that commit" ;;
|
|
67
|
+
"$PHASE_EXIT_HALT_REVIEW") echo "HALT: the reviewer never approved the change and the ladder is exhausted — human review required, a retry cannot help" ;;
|
|
68
|
+
*) echo "failed (exit ${1:-?})" ;;
|
|
69
|
+
esac
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# phase_retry_mark <codeline-root> — what the codeline holds before a self-heal retry (its commit), for
|
|
73
|
+
# phase_retry_progressed. Empty when the root is not a repository.
|
|
74
|
+
phase_retry_mark() {
|
|
75
|
+
git -C "${1:-.}" rev-parse -q --verify HEAD 2>/dev/null || true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# phase_retry_progressed <codeline-root> <mark> — true when the retry changed what the codeline holds.
|
|
79
|
+
#
|
|
80
|
+
# THE PIPELINE NEVER LOOPS (operator, 2026-10-14: "pipeline should NEVER get into a loop!!!"). A retry is repeated
|
|
81
|
+
# while its gates remediate; AMSD-2974 live run L2 retried the 7.x line five times — each time the remediator added
|
|
82
|
+
# criteria, the writer committed nothing, the same code failed the same gate (~$2.50 a round). A retry that changed
|
|
83
|
+
# nothing cannot make the next one different: it is not repeated. With no mark (no repository), progress cannot be
|
|
84
|
+
# told, and the retry is not repeated either.
|
|
85
|
+
phase_retry_progressed() {
|
|
86
|
+
local _now
|
|
87
|
+
[ -n "${2:-}" ] || return 1
|
|
88
|
+
_now=$(phase_retry_mark "${1:-.}")
|
|
89
|
+
[ -n "$_now" ] && [ "$_now" != "$2" ]
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pipeline_commit_flags — the flags every commit the PIPELINE makes passes to `git commit`, one per line.
|
|
3
|
+
#
|
|
4
|
+
# A codeline's hooks are written for its team's own checkout and machine. Operator decisions,
|
|
5
|
+
# 2026-10-05 (AMSD-2974): the pipeline's commits skip them — first inside worktrees, then in the
|
|
6
|
+
# checkout too. Proven on a copy of next.gotransit.com: its pre-commit hook (husky + lint-staged)
|
|
7
|
+
# type-checks the WHOLE project, which fails on the untouched checkout with 6 errors because the
|
|
8
|
+
# team's node_modules is older than its own develop branch expects (it needs an `npm install`, which
|
|
9
|
+
# the pipeline may not run) — so the hook refused every pipeline commit there, whatever it held, and
|
|
10
|
+
# the reproducing test of a correct fix was never committed. In worktrees the same hook also refused
|
|
11
|
+
# the branch name and sourced a helper git does not track.
|
|
12
|
+
#
|
|
13
|
+
# What the hook checks is still checked by the pipeline: the story's own verification, Step 19/20's
|
|
14
|
+
# type check judged against the phase baseline, the lint gate, review and the QA gates. The Step 17
|
|
15
|
+
# merge onto the story branch still runs the codeline's commit-msg hook (git merge has no --no-verify
|
|
16
|
+
# here), and its message is written to pass it.
|
|
17
|
+
pipeline_commit_flags() {
|
|
18
|
+
echo "--no-verify"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* planHash(plan) — the story's plan, identified: md5 of its JSON with keys sorted at every level; '' when there is
|
|
4
|
+
* none. What a spec was built under (story.specification.planHash) is compared with the plan the story has now.
|
|
5
|
+
*/
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
function stable(v) {
|
|
9
|
+
if (Array.isArray(v)) return `[${v.map(stable).join(',')}]`;
|
|
10
|
+
if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stable(v[k])}`).join(',')}}`;
|
|
11
|
+
return JSON.stringify(v);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function planHash(plan) {
|
|
15
|
+
if (!plan || typeof plan !== 'object') return '';
|
|
16
|
+
return crypto.createHash('md5').update(stable(plan)).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { planHash };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* THE PLAN DECIDES THE CODELINES A STORY IS WORKED IN.
|
|
4
|
+
*
|
|
5
|
+
* Live AMSD-2974 run 1, 2026-10-09: discovery grounded four codelines; the AC gate's router picked one; the detective was
|
|
6
|
+
* shown that one ("No other codeline is in scope for this investigation") and planned no consumer and no back-port. The
|
|
7
|
+
* plan — written once, by the detective that reads the code — is the decision on which codelines change and which only
|
|
8
|
+
* receive (operator, 2026-10-06); the router's pick is only where the story starts.
|
|
9
|
+
*
|
|
10
|
+
* Both answers come from the run's own record of what discovery grounded (codeline-facts.json: one top-level key per
|
|
11
|
+
* codeline, each with its path, rewritten every run). Nothing is invented: a codeline the record does not hold is never
|
|
12
|
+
* given a lane, and a lane the story already has is never taken away.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The codelines discovery grounded this run, with their paths, in the record's order. */
|
|
16
|
+
function discovered(facts) {
|
|
17
|
+
if (!facts || typeof facts !== 'object') return [];
|
|
18
|
+
return Object.entries(facts)
|
|
19
|
+
.filter(([k, v]) => !k.startsWith('_') && v && typeof v === 'object' && typeof v.path === 'string' && v.path)
|
|
20
|
+
.map(([codeline, v]) => ({ codeline, path: v.path }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function storyLanes(story) {
|
|
24
|
+
return (Array.isArray(story && story.codelines) && story.codelines.length ? story.codelines : [story && story.codeline])
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* planScope(story, prd, facts) → { codelines, outputDirs }: what the detective writing the plan is shown — the story's
|
|
30
|
+
* own codelines first, then every other codeline discovery grounded, each with where it is.
|
|
31
|
+
*/
|
|
32
|
+
function planScope(story, prd, facts) {
|
|
33
|
+
const dirs = (prd && prd.project && Array.isArray(prd.project.outputDirs)) ? prd.project.outputDirs : [];
|
|
34
|
+
const found = discovered(facts);
|
|
35
|
+
const codelines = [...new Set([...storyLanes(story), ...found.map((f) => f.codeline)])];
|
|
36
|
+
const outputDirs = codelines.map((cl) => dirs.find((d) => d && d.codeline === cl) || found.find((f) => f.codeline === cl))
|
|
37
|
+
.filter(Boolean).map((d) => ({ codeline: d.codeline, path: d.path }));
|
|
38
|
+
return { codelines, outputDirs };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Every codeline a plan names: its parts in plan order, then each package's provider and consumers. */
|
|
42
|
+
function plannedCodelines(plan) {
|
|
43
|
+
if (!plan || typeof plan !== 'object') return [];
|
|
44
|
+
const parts = (Array.isArray(plan.codelines) ? plan.codelines : []).slice()
|
|
45
|
+
.sort((a, b) => (Number(a && a.order) || 0) - (Number(b && b.order) || 0)).map((c) => c && c.codeline);
|
|
46
|
+
const pkgs = Array.isArray(plan.packages) ? plan.packages : [];
|
|
47
|
+
const around = pkgs.flatMap((p) => [p && p.providedBy, ...((p && Array.isArray(p.consumers)) ? p.consumers.map((c) => c && c.codeline) : [])]);
|
|
48
|
+
return [...new Set([...parts, ...around].filter(Boolean))];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* adoptPlanCodelines(story, prd, facts) → the codelines added: every codeline the story's plan names that discovery
|
|
53
|
+
* grounded becomes one of the story's codelines (story.codelines, routed one first) and one of the run's
|
|
54
|
+
* project.outputDirs, with its path. Mutates story and prd.
|
|
55
|
+
*/
|
|
56
|
+
function adoptPlanCodelines(story, prd, facts) {
|
|
57
|
+
if (!story || !story.overallPlan) return [];
|
|
58
|
+
const found = discovered(facts);
|
|
59
|
+
const have = storyLanes(story);
|
|
60
|
+
const added = plannedCodelines(story.overallPlan)
|
|
61
|
+
.filter((cl) => !have.includes(cl) && found.some((f) => f.codeline === cl));
|
|
62
|
+
if (!added.length) return [];
|
|
63
|
+
story.codelines = [...have, ...added];
|
|
64
|
+
prd.project = prd.project || {};
|
|
65
|
+
const dirs = Array.isArray(prd.project.outputDirs) ? prd.project.outputDirs : [];
|
|
66
|
+
for (const cl of added) {
|
|
67
|
+
if (!dirs.some((d) => d && d.codeline === cl)) dirs.push({ codeline: cl, path: found.find((f) => f.codeline === cl).path });
|
|
68
|
+
}
|
|
69
|
+
prd.project.outputDirs = dirs;
|
|
70
|
+
return added;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { planScope, adoptPlanCodelines, plannedCodelines, discovered };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# WHAT A QA GATE IS SHOWN DECIDES WHAT IT CAN CONCLUDE.
|
|
3
|
+
#
|
|
4
|
+
# Live 2026-09-09, run 20260908T215555Z — AMSD-1919, a one-line brownfield defect fixed at
|
|
5
|
+
# CheckoutForm.tsx:306. Two gates were paid for and neither could reach a verdict about the change:
|
|
6
|
+
#
|
|
7
|
+
# qa-gate:runtime-boundary "CheckoutForm.tsx itself is unmodified (git diff confirms no
|
|
8
|
+
# changes)." — false, and stated with confidence
|
|
9
|
+
# qa-gate:mutant-hunter "truncated to the first 100 of 436 lines and does not include the
|
|
10
|
+
# case-sensitivity comparison logic around line 306" — true, and
|
|
11
|
+
# the reason it could only answer WARN
|
|
12
|
+
#
|
|
13
|
+
# Two different faults, one shape: the evidence window could not contain the answer.
|
|
14
|
+
#
|
|
15
|
+
# 1. The diff was built as `git diff "${_rev_base:-HEAD~1}" HEAD`, and `_rev_base=` appears
|
|
16
|
+
# nowhere in the engine. The fallback was not a fallback, it was the only behaviour — and
|
|
17
|
+
# HEAD~1..HEAD is the last commit alone. repro-test-writer always commits AFTER the writer,
|
|
18
|
+
# so on every brownfield defect that window holds the reproducing test and never the fix.
|
|
19
|
+
#
|
|
20
|
+
# 2. The excerpt was `head -n <window>` — anchored at the top of the file, so a 436-line
|
|
21
|
+
# component whose change sits at line 306 is out of reach at any window size. An earlier fix
|
|
22
|
+
# added a truncation NOTICE, which is why the agent refused honestly rather than guessing;
|
|
23
|
+
# it did not make the agent see.
|
|
24
|
+
#
|
|
25
|
+
# Both are fixed by deriving, never by declaring a number here: the base is the phase baseline the
|
|
26
|
+
# run itself recorded, and every window size stays in config/evidence-windows.json.
|
|
27
|
+
#
|
|
28
|
+
# Sourced (not executed) — every function prints to stdout and returns non-zero when it cannot
|
|
29
|
+
# produce evidence, so a caller can say "no evidence" instead of shipping a misleading window.
|
|
30
|
+
|
|
31
|
+
# qa_gate_diff <project_root> <log_dir>
|
|
32
|
+
#
|
|
33
|
+
# The diff a QA gate must judge: everything THIS PHASE changed, from the baseline the phase
|
|
34
|
+
# recorded. story_outputs_baseline_ref is the single reader of phase-baseline-sha.txt and already
|
|
35
|
+
# serves story_outputs_deleted and story_outputs_files; this is the third consumer, not a second
|
|
36
|
+
# way of answering the same question.
|
|
37
|
+
#
|
|
38
|
+
# ABSENT IS NOT AN EMPTY DIFF. A missing or unusable baseline returns non-zero WITH a sentence
|
|
39
|
+
# saying so, because "nothing changed" and "I could not tell what changed" are opposite findings
|
|
40
|
+
# and the gate acts on them differently.
|
|
41
|
+
qa_gate_diff() {
|
|
42
|
+
local _root="${1:-}" _log="${2:-}"
|
|
43
|
+
if [ -z "$_root" ] || [ -z "$_log" ]; then
|
|
44
|
+
printf '(qa_gate_diff: no project root or log dir given — this gate has no diff to judge)'
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
local _base=""
|
|
49
|
+
if command -v story_outputs_baseline_ref >/dev/null 2>&1; then
|
|
50
|
+
_base=$(story_outputs_baseline_ref "$_log" 2>/dev/null) || _base=""
|
|
51
|
+
fi
|
|
52
|
+
if [ -z "$_base" ]; then
|
|
53
|
+
printf '(no phase baseline was recorded for this run — this gate has no diff to judge, and MUST NOT read that as "nothing changed")'
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if ! git -C "$_root" rev-parse --verify --quiet "${_base}^{commit}" >/dev/null 2>&1; then
|
|
58
|
+
printf '(the recorded phase baseline %s is not a commit in this repository — this gate has no diff to judge)' "$_base"
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# A BASELINE THAT IS NOT AN ANCESTOR OF HEAD CANNOT DESCRIBE THIS PHASE.
|
|
63
|
+
#
|
|
64
|
+
# Live 2026-09-09, the second resume of run 20260908T215555Z: the baseline was recorded before
|
|
65
|
+
# ensure_story_branch hard-reset the story branch onto origin/<baseline>, so it named a commit
|
|
66
|
+
# from the PREVIOUS leg that the reset then orphaned. Diffing an orphan is not empty — it is a
|
|
67
|
+
# cross-branch comparison, and it produced 8 insertions and 11 deletions in the reproducing
|
|
68
|
+
# test with no sign of the source fix. runtime-boundary read that and reported the change was
|
|
69
|
+
# "confined to a Jest/RTL spec file" for the second run running.
|
|
70
|
+
#
|
|
71
|
+
# Refused rather than diffed: this whole file exists to keep "nothing changed" apart from
|
|
72
|
+
# "I cannot tell what changed", and a plausible wrong diff is the worse of the two.
|
|
73
|
+
if ! git -C "$_root" merge-base --is-ancestor "$_base" HEAD >/dev/null 2>&1; then
|
|
74
|
+
printf '(the recorded phase baseline %s is not an ancestor of HEAD — it does not describe this phase, so this gate has no diff to judge. A branch reset after the baseline was recorded is the usual cause.)' "$_base"
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# The size is DECLARED, like every other evidence window. A literal here would be the exact
|
|
79
|
+
# thing config/evidence-windows.json exists to stop.
|
|
80
|
+
local _cap
|
|
81
|
+
_cap=$(evidence_window qaGateDiffLines 2>/dev/null) || _cap=""
|
|
82
|
+
if [ -z "$_cap" ]; then
|
|
83
|
+
printf '(the qaGateDiffLines evidence window is not declared — refusing to invent a size)'
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
git -C "$_root" diff "$_base" HEAD 2>/dev/null | head -n "$_cap"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# qa_gate_excerpt <project_root> <log_dir> <repo_relative_path> <window_name>
|
|
91
|
+
#
|
|
92
|
+
# The part of a file a gate needs: the region THIS PHASE changed, with the declared window's worth
|
|
93
|
+
# of surrounding context — never the first N lines, which is a window on whatever the file happens
|
|
94
|
+
# to open with.
|
|
95
|
+
#
|
|
96
|
+
# A file that fits inside its window is printed whole and says nothing about truncation: claiming
|
|
97
|
+
# a cut that did not happen is its own kind of misleading.
|
|
98
|
+
qa_gate_excerpt() {
|
|
99
|
+
local _root="${1:-}" _log="${2:-}" _rel="${3:-}" _win_name="${4:-}"
|
|
100
|
+
# CALLERS HOLD BOTH SHAPES. story_outputs_sources yields repo-relative paths and the
|
|
101
|
+
# find-fallback yields absolute ones; a function that accepted only one silently produced
|
|
102
|
+
# "(unreadable)" for the other, which reads exactly like a file that is not there.
|
|
103
|
+
local _abs
|
|
104
|
+
case "$_rel" in
|
|
105
|
+
/*) _abs="$_rel"; _rel="${_rel#"$_root"/}" ;;
|
|
106
|
+
*) _abs="$_root/$_rel" ;;
|
|
107
|
+
esac
|
|
108
|
+
[ -f "$_abs" ] || { printf '(%s: unreadable)' "$_rel"; return 1; }
|
|
109
|
+
|
|
110
|
+
local _window
|
|
111
|
+
_window=$(evidence_window "$_win_name" 2>/dev/null) || _window=""
|
|
112
|
+
if [ -z "$_window" ] || [ "$_window" -le 0 ] 2>/dev/null; then
|
|
113
|
+
printf '(the %s evidence window is not declared — refusing to invent a size)' "$_win_name"
|
|
114
|
+
return 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
local _total
|
|
118
|
+
_total=$(wc -l < "$_abs" 2>/dev/null | tr -d '[:space:]')
|
|
119
|
+
[ -n "$_total" ] || _total=0
|
|
120
|
+
|
|
121
|
+
printf -- '--- %s' "$_rel"
|
|
122
|
+
|
|
123
|
+
# Whole file, no claim of truncation.
|
|
124
|
+
if [ "$_total" -le "$_window" ]; then
|
|
125
|
+
printf -- ' ---\n'
|
|
126
|
+
cat "$_abs"
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# WHERE THE CHANGE IS, asked of git rather than assumed. -U0 so the hunk headers name only
|
|
131
|
+
# lines that actually changed; the surrounding context is this function's to decide.
|
|
132
|
+
local _first="" _last="" _base=""
|
|
133
|
+
if command -v story_outputs_baseline_ref >/dev/null 2>&1; then
|
|
134
|
+
_base=$(story_outputs_baseline_ref "$_log" 2>/dev/null) || _base=""
|
|
135
|
+
fi
|
|
136
|
+
if [ -n "$_base" ] && git -C "$_root" rev-parse --verify --quiet "${_base}^{commit}" >/dev/null 2>&1; then
|
|
137
|
+
local _hdr _start _count
|
|
138
|
+
while IFS= read -r _hdr; do
|
|
139
|
+
# @@ -a,b +c,d @@ — c is the first changed line on the new side, d how many.
|
|
140
|
+
_start=$(printf '%s' "$_hdr" | sed -n 's/^@@ -[0-9,]* +\([0-9]*\).*/\1/p')
|
|
141
|
+
_count=$(printf '%s' "$_hdr" | sed -n 's/^@@ -[0-9,]* +[0-9]*,\([0-9]*\).*/\1/p')
|
|
142
|
+
[ -n "$_start" ] || continue
|
|
143
|
+
[ -n "$_count" ] || _count=1
|
|
144
|
+
[ "$_count" -gt 0 ] 2>/dev/null || _count=1
|
|
145
|
+
local _end=$(( _start + _count - 1 ))
|
|
146
|
+
{ [ -z "$_first" ] || [ "$_start" -lt "$_first" ]; } && _first="$_start"
|
|
147
|
+
{ [ -z "$_last" ] || [ "$_end" -gt "$_last" ]; } && _last="$_end"
|
|
148
|
+
done < <(git -C "$_root" diff -U0 "$_base" HEAD -- "$_rel" 2>/dev/null | grep '^@@')
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
local _from _to _anchored="around this phase's change"
|
|
152
|
+
if [ -n "$_first" ] && [ -n "$_last" ]; then
|
|
153
|
+
# Centre the window on the changed region.
|
|
154
|
+
local _span=$(( _last - _first + 1 ))
|
|
155
|
+
local _pad=$(( (_window - _span) / 2 ))
|
|
156
|
+
[ "$_pad" -lt 0 ] && _pad=0
|
|
157
|
+
_from=$(( _first - _pad ))
|
|
158
|
+
[ "$_from" -lt 1 ] && _from=1
|
|
159
|
+
_to=$(( _from + _window - 1 ))
|
|
160
|
+
if [ "$_to" -gt "$_total" ]; then
|
|
161
|
+
_to="$_total"
|
|
162
|
+
_from=$(( _to - _window + 1 ))
|
|
163
|
+
[ "$_from" -lt 1 ] && _from=1
|
|
164
|
+
fi
|
|
165
|
+
else
|
|
166
|
+
# NAMED, not silent. Falling back to the head of the file is the old behaviour, and the
|
|
167
|
+
# gate must be told that is what it is looking at.
|
|
168
|
+
_from=1
|
|
169
|
+
_to="$_window"
|
|
170
|
+
[ "$_to" -gt "$_total" ] && _to="$_total"
|
|
171
|
+
_anchored="from the START of the file — this phase's changed lines could not be located"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
printf -- ' (lines %s-%s of %s, %s) ---\n' "$_from" "$_to" "$_total" "$_anchored"
|
|
175
|
+
sed -n "${_from},${_to}p" "$_abs"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# qa_phase_baseline_sha <project_root> [baseline_branch]
|
|
179
|
+
#
|
|
180
|
+
# THE COMMIT THIS PHASE'S WORK DIVERGED FROM, which is not the same as HEAD.
|
|
181
|
+
#
|
|
182
|
+
# run-agent-orchestration.sh captured `rev-parse HEAD` before the story loop, and
|
|
183
|
+
# ensure_story_branch then hard-reset the story branch onto origin/<baseline> — orphaning the very
|
|
184
|
+
# commit just recorded. On a FIRST run the two agree, because HEAD is already the base; on every
|
|
185
|
+
# RESUME the recorded baseline is the previous leg's tip and every diff-based gate reads it:
|
|
186
|
+
# review-ranger, mutant-hunter, fuzz-weaver, sast-sentinel, runtime-boundary and the tsc/eslint
|
|
187
|
+
# baseline gates.
|
|
188
|
+
#
|
|
189
|
+
# The merge-base gives the same answer before and after the reset, which is what makes it a
|
|
190
|
+
# baseline rather than a snapshot of whatever the branch happened to be carrying.
|
|
191
|
+
#
|
|
192
|
+
# NOTHING IS INVENTED. A base branch that does not resolve falls back to HEAD — the previous
|
|
193
|
+
# behaviour exactly — rather than to a guessed ref.
|
|
194
|
+
qa_phase_baseline_sha() {
|
|
195
|
+
local _root="${1:-}" _branch="${2:-}"
|
|
196
|
+
[ -n "$_root" ] || return 1
|
|
197
|
+
|
|
198
|
+
local _head
|
|
199
|
+
_head=$(git -C "$_root" rev-parse --verify --quiet HEAD 2>/dev/null) || _head=""
|
|
200
|
+
[ -n "$_head" ] || return 1
|
|
201
|
+
|
|
202
|
+
if [ -n "$_branch" ]; then
|
|
203
|
+
# origin/<branch> first, as ensure_story_branch resolves it, then the bare name.
|
|
204
|
+
local _ref
|
|
205
|
+
for _ref in "origin/$_branch" "$_branch"; do
|
|
206
|
+
git -C "$_root" rev-parse --verify --quiet "${_ref}^{commit}" >/dev/null 2>&1 || continue
|
|
207
|
+
local _mb
|
|
208
|
+
_mb=$(git -C "$_root" merge-base "$_head" "$_ref" 2>/dev/null) || _mb=""
|
|
209
|
+
if [ -n "$_mb" ]; then
|
|
210
|
+
printf '%s' "$_mb"
|
|
211
|
+
return 0
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
printf '%s' "$_head"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# qa_record_phase_baseline <root> <base-branch> <log-dir> — WRITE THE PHASE BASELINE every gate reads.
|
|
220
|
+
#
|
|
221
|
+
# One recording for both ways a phase runs its stories. It lived only in Step 8 (main-branch stories),
|
|
222
|
+
# so a brownfield lane that runs its story in a worktree — and skips Step 8 — had no
|
|
223
|
+
# phase-baseline-sha.txt: every pre-existing failure was charged to its writer, eight attempts in a
|
|
224
|
+
# row (£0 multi-lane run, 2026-10-04). Nothing resolvable is said out loud, never written as a guess.
|
|
225
|
+
qa_record_phase_baseline() {
|
|
226
|
+
local _root="${1:-}" _branch="${2:-}" _log_dir="${3:-${LOG_DIR:-}}" _sha
|
|
227
|
+
[ -n "$_root" ] && [ -n "$_log_dir" ] || return 1
|
|
228
|
+
_sha="$(qa_phase_baseline_sha "$_root" "$_branch" 2>/dev/null || echo "")"
|
|
229
|
+
if [ -z "$_sha" ]; then
|
|
230
|
+
warning "[orch] could not resolve a baseline commit in $_root — every diff-based gate will compare against nothing"
|
|
231
|
+
return 1
|
|
232
|
+
fi
|
|
233
|
+
printf '%s\n' "$_sha" > "$_log_dir/phase-baseline-sha.txt"
|
|
234
|
+
printf '%s' "$_sha"
|
|
235
|
+
}
|