feat: implement spec 439 branch and evidence truth
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 7m2s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 7m2s
This commit is contained in:
parent
71e3745aee
commit
8144a3be78
@ -20,11 +20,19 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run UI/Productization Coverage guard
|
||||
env:
|
||||
TENANTATLAS_PR_TARGET: ${{ github.base_ref }}
|
||||
run: |
|
||||
git fetch origin dev --depth=1
|
||||
bash scripts/check-ui-productization-coverage origin/dev
|
||||
if [[ -z "${TENANTATLAS_PR_TARGET}" ]]; then
|
||||
echo "PR target is unavailable; refusing to choose a fallback branch." >&2
|
||||
exit 2
|
||||
fi
|
||||
git fetch origin "${TENANTATLAS_PR_TARGET}:refs/remotes/origin/${TENANTATLAS_PR_TARGET}"
|
||||
bash scripts/check-ui-productization-coverage "origin/${TENANTATLAS_PR_TARGET}" --pull-request-target="${TENANTATLAS_PR_TARGET}"
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
@ -1,41 +1,40 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
|
||||
- Version change: 2.15.0 -> 2.16.0
|
||||
- Version change: 2.16.0 -> 3.0.0
|
||||
- Rationale:
|
||||
- The former universal `dev` feature-base rule conflicts with the repository's
|
||||
current platform and website integration streams. This amendment replaces
|
||||
that rule with explicit scope-specific branch roles and fail-closed routing.
|
||||
- Modified principles:
|
||||
- First Provider Is Not Platform Core (PROV-001): clarified by ownership
|
||||
schema alignment so provider-native identifiers remain metadata, not
|
||||
platform-core ownership truth
|
||||
- Scope & Ownership Clarification (SCOPE-001): amended from a tenant_id
|
||||
database convention to workspace + managed_environment ownership
|
||||
- Pre-Production Lean Doctrine (LEAN-001): reinforced by banning tenant_id
|
||||
compatibility aliases, dual-write targets, fallback readers, and parallel
|
||||
ownership truth
|
||||
- Spec-First Workflow: redefined branch topology so ordinary platform and
|
||||
website work uses its own integration branch while repository-wide
|
||||
integration and promotion remain explicit `dev` work.
|
||||
- Added sections: None
|
||||
- Removed sections: None
|
||||
- Templates requiring updates:
|
||||
- ✅ `.specify/templates/*.md`, `.specify/README.md`, `AGENTS.md`, and
|
||||
`README.md`: no old mandatory `workspace_id` + `tenant_id` database
|
||||
convention found
|
||||
- ⚠ pending `docs/HANDOVER.md`: historical schema inventory still contains
|
||||
legacy `tenant_id` table/column references; docs cleanup is outside this
|
||||
constitution-only task
|
||||
- ⚠ pending `docs/research/golden-master-baseline-drift-deep-analysis.md`,
|
||||
`docs/audits/filter-audit-comprehensive.md`, and
|
||||
`docs/audits/2026-03-09-enterprise-rbac-scope-audit.md`: historical
|
||||
research/audit references still mention `tenant_id`; review separately if
|
||||
they are promoted back into current guidance
|
||||
- CI/script updates: None
|
||||
- Templates/specs/instructions reviewed:
|
||||
- ✅ `.specify/templates/*.md` and `.specify/README.md`: no universal `dev`
|
||||
feature-base mapping requires a template change.
|
||||
- ✅ `specs/439-branch-topology-local-evidence-truth/`: already defines the
|
||||
same three branch roles and the mandatory amendment-first phase gate.
|
||||
- ✅ `.codex/skills/platform-feature-finish/SKILL.md` and
|
||||
`.codex/skills/giteaflow/SKILL.md`: existing platform-target guidance is
|
||||
compatible with this amendment; no Phase 1 change required.
|
||||
- ✅ `Agents.md`: directly conflicting universal `dev` instructions were
|
||||
synchronized in Spec 439 Phase 2 after the Phase 1 gate passed.
|
||||
- ✅ Specs 437 and 438: historical/completed or deferred context remains
|
||||
unchanged.
|
||||
- CI/script updates:
|
||||
- None in this amendment. Spec 439 authorizes dependent resolver, guard, and
|
||||
evidence-truth work only after Phase 1 validation.
|
||||
- Commands checked:
|
||||
- N/A `.specify/templates/commands/*.md` directory is not present
|
||||
- N/A `.specify/templates/commands/*.md` directory is not present.
|
||||
- SemVer decision:
|
||||
- MAJOR because a normative universal branch rule is redefined in a
|
||||
backward-incompatible way; PATCH and MINOR would understate the change.
|
||||
- Follow-up TODOs:
|
||||
- Spec 414 still contains `tenant_id` terminology and must be patched in
|
||||
its own task to use `workspace_id` + `managed_environment_id` +
|
||||
`provider_connection_id`
|
||||
- This amendment does not migrate runtime code. Current database schema
|
||||
scan found provider-native `entra_tenant_id` metadata columns and no
|
||||
literal `tenant_id` columns; source-level `tenant_id` references remain
|
||||
outside this constitution-only task.
|
||||
- Unrelated historical documentation and skill cleanup remain outside this
|
||||
amendment.
|
||||
-->
|
||||
|
||||
# TenantPilot Constitution
|
||||
@ -1886,7 +1885,10 @@ ### Incremental UI Standards Enforcement (UI-STD-001)
|
||||
|
||||
### Spec-First Workflow
|
||||
- For any feature that changes runtime behavior, include or update `specs/<NNN>-<slug>/` with `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
|
||||
- New work branches from `dev` using `feat/<NNN>-<slug>` (spec + code in the same PR).
|
||||
- Long-lived branch roles are explicit: `dev` is the repository-wide integration and promotion branch, `platform-dev` is the integration branch for TenantPilot platform work, and `website-dev` is the integration branch for website work.
|
||||
- Ordinary platform work MUST branch from, target, and diff against `platform-dev`; ordinary website work MUST branch from, target, and diff against `website-dev`.
|
||||
- Cross-stream integration, promotion, and repository-governance work MUST be explicitly classified and MUST declare its feature base, target, and diff baseline when no unambiguous scope-specific rule applies. Such work MAY target `dev`; the existence of `dev` alone MUST NOT imply that target.
|
||||
- Ambiguous or missing branch-family/ref resolution MUST fail closed. Platform or website work MUST NOT silently fall back to `dev`, `origin/dev`, the wrong branch family, `HEAD~1`, or an arbitrary prior commit.
|
||||
|
||||
## Quality Gates
|
||||
- Changes MUST be programmatically tested (Pest) and run via targeted `php artisan test ...`.
|
||||
@ -1934,4 +1936,4 @@ ### Versioning Policy (SemVer)
|
||||
- **MINOR**: new principle/section or materially expanded guidance.
|
||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||
|
||||
**Version**: 2.16.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-06-25
|
||||
**Version**: 3.0.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-07-11
|
||||
|
||||
59
Agents.md
59
Agents.md
@ -38,8 +38,17 @@ ## Workflow (Spec Kit)
|
||||
## Workflow (SDD in diesem Repo)
|
||||
|
||||
### Branching
|
||||
- Default / Integrations-Branch: `dev`
|
||||
- Neue Arbeit läuft über Feature-Branches von `dev`:
|
||||
- Langfristige Branchrollen:
|
||||
- `dev`: repository-weite Integration und Promotion
|
||||
- `platform-dev`: Integration für TenantPilot-Plattformarbeit
|
||||
- `website-dev`: Integration für Website-Arbeit
|
||||
- Normale Plattformarbeit startet von und zielt auf `platform-dev`; normale
|
||||
Website-Arbeit startet von und zielt auf `website-dev`.
|
||||
- Cross-Stream-, Promotion- und Repository-Governance-Arbeit muss ausdrücklich
|
||||
klassifiziert werden. Wenn keine eindeutige Scope-Regel greift, müssen Base,
|
||||
Target und Diff-Baseline explizit angegeben werden; `dev` darf dabei ein
|
||||
bewusstes Integrations-/Promotion-Ziel sein, aber kein stiller Fallback.
|
||||
- Neue Arbeit läuft über Feature-Branches der jeweils passenden Basis:
|
||||
- `feat/<NNN>-<slug>` (Code + Spec im selben PR)
|
||||
- optional: `spec/<NNN>-<slug>` (nur wenn wir Specs getrennt reviewen wollen)
|
||||
|
||||
@ -49,14 +58,18 @@ ### Wo liegen Specs?
|
||||
- `specs/<NNN>-<slug>/plan.md`
|
||||
- `specs/<NNN>-<slug>/tasks.md`
|
||||
- `specs/<NNN>-<slug>/spec.md`
|
||||
- `specs/` muss im `dev`-Branch immer existieren (Baseline).
|
||||
- `specs/` bleibt Bestandteil der Repository-Baseline; das aktive Spec-Paket
|
||||
wird im selben Feature-Branch und PR wie die zugehörige Änderung geführt.
|
||||
|
||||
### Variante B Standard (Spec + Code in einem PR)
|
||||
1) Branch von `dev` erstellen: `feat/<NNN>-<slug>`
|
||||
1) Scope bestimmen und Branch von der passenden Basis erstellen:
|
||||
`platform-dev` für Plattformarbeit, `website-dev` für Website-Arbeit oder
|
||||
eine explizit deklarierte Basis für Cross-Stream-/Promotion-/Governance-Arbeit
|
||||
2) Zuerst Specs erstellen/aktualisieren → erster Commit (`spec:`)
|
||||
3) Dann implementieren → weitere Commits (`feat:`, `fix:`, `test:`)
|
||||
4) PR/MR: `feat/...` → `dev`
|
||||
5) Merge nach `dev` (empfohlen: Squash)
|
||||
4) PR/MR: `feat/...` → deklariertes normales Ziel
|
||||
5) Merge in das deklarierte Ziel (empfohlen: Squash); eine spätere Promotion
|
||||
nach `dev` ist separate, ausdrücklich klassifizierte Integrationsarbeit
|
||||
|
||||
### Gate-Regel
|
||||
- Wenn Code geändert wird (z.B. `app/`, `config/`, `database/`, `resources/`),
|
||||
@ -152,18 +165,27 @@ # Or stash conflicting changes
|
||||
|
||||
## Solo + Copilot Workflow (Konflikte vermeiden)
|
||||
|
||||
Wenn du alleine arbeitest (du + Copilot), sind große Konflikt-Stürme fast immer „Branch drift“: `dev` bewegt sich weiter, das Feature hängt hinterher. Diese Regeln halten Feature-Branches mergebar.
|
||||
Wenn du alleine arbeitest (du + Copilot), sind große Konflikt-Stürme fast immer
|
||||
„Branch drift“: Die für das Feature deklarierte Integrationsbasis bewegt sich
|
||||
weiter, das Feature hängt hinterher. Diese Regeln halten Feature-Branches
|
||||
mergebar.
|
||||
|
||||
### Regel 1: Vor jeder Troubleshooting-Änderung zuerst `dev` ins Feature holen
|
||||
### Regel 1: Vor jeder Troubleshooting-Änderung zuerst die deklarierte Basis ins Feature holen
|
||||
|
||||
Bevor du einen kleinen Fix auf einem Feature-Branch machst (z.B. `config/`, `tests/`, shared Services), synchronisiere:
|
||||
Bevor du einen kleinen Fix auf einem Feature-Branch machst (z.B. `config/`,
|
||||
`tests/`, shared Services), synchronisiere mit der für den Scope passenden,
|
||||
vorab deklarierten Integrationsbasis:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout feat/<NNN>-<slug>
|
||||
git merge origin/dev
|
||||
git merge origin/<declared-integration-branch>
|
||||
```
|
||||
|
||||
Für normale Plattformarbeit ist das `origin/platform-dev`, für normale
|
||||
Website-Arbeit `origin/website-dev`. `origin/dev` ist nur für ausdrücklich als
|
||||
repository-weite Integration oder Promotion klassifizierte Arbeit zulässig.
|
||||
|
||||
### Regel 2: Kurzlebige „Session Branches“ auch im Solo-Setup
|
||||
|
||||
Auch wenn du alleine bist: nutze Session-Branches für gezielte Fixes, damit du jederzeit sauber abbrechen kannst.
|
||||
@ -182,18 +204,25 @@ ### Regel 2: Kurzlebige „Session Branches“ auch im Solo-Setup
|
||||
git merge $SESSION_BRANCH --no-ff -m "merge: agent session work"
|
||||
```
|
||||
|
||||
### Regel 3: „Globale“ Fixes als Mini-PR nach `dev`
|
||||
### Regel 3: Geteilte Fixes nach Scope klassifizieren
|
||||
|
||||
Wenn ein Fix nicht wirklich feature-spezifisch ist (z.B. `config/graph_contracts.php`, Test-Bootstrap, allgemeine Graph-Validation), dann:
|
||||
|
||||
- Mini-Branch von `dev` erstellen und PR → `dev` mergen.
|
||||
- Danach im Feature-Branch einfach wieder `origin/dev` mergen.
|
||||
- Plattformbezogene Fixes von `platform-dev` erstellen und dorthin mergen;
|
||||
websitebezogene Fixes entsprechend von/nach `website-dev`.
|
||||
- Echte Cross-Stream- oder repository-weite Integrationsfixes ausdrücklich
|
||||
klassifizieren und Base, Ziel sowie Diff-Baseline deklarieren; sie dürfen
|
||||
gezielt nach `dev` gehen.
|
||||
- Danach im Feature-Branch wieder die deklarierte Integrationsbasis mergen.
|
||||
|
||||
Das reduziert Add/Add-Konflikte drastisch, weil `dev` die gemeinsame Wahrheit bleibt.
|
||||
Das reduziert Add/Add-Konflikte, ohne `dev` fälschlich als universelle
|
||||
Stream-Basis zu behandeln.
|
||||
|
||||
### Regel 4: Kein Rebase auf geteilten Branches
|
||||
|
||||
Wenn du und Copilot über längere Zeit auf demselben Feature-Branch arbeiten, bleib bei `merge origin/dev` (kein Rebase), damit die Historie stabil bleibt.
|
||||
Wenn du und Copilot über längere Zeit auf demselben Feature-Branch arbeiten,
|
||||
bleib bei `merge origin/<declared-integration-branch>` (kein Rebase), damit die
|
||||
Historie stabil bleibt.
|
||||
|
||||
## Architecture Assumptions
|
||||
- Backend: Laravel (latest stable)
|
||||
|
||||
@ -0,0 +1,623 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
function branchTopologyGit(string $repository, string ...$arguments): string
|
||||
{
|
||||
$process = new Process(['git', ...$arguments], $repository);
|
||||
$process->setTimeout(15);
|
||||
$process->run();
|
||||
|
||||
expect($process->isSuccessful())
|
||||
->toBeTrue(trim($process->getErrorOutput()) ?: trim($process->getOutput()));
|
||||
|
||||
return trim($process->getOutput());
|
||||
}
|
||||
|
||||
function branchTopologyCommit(string $repository, string $path, string $contents, string $message): string
|
||||
{
|
||||
$absolutePath = $repository.DIRECTORY_SEPARATOR.$path;
|
||||
$directory = dirname($absolutePath);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($absolutePath, $contents);
|
||||
branchTopologyGit($repository, 'add', $path);
|
||||
branchTopologyGit($repository, 'commit', '-m', $message);
|
||||
|
||||
return branchTopologyGit($repository, 'rev-parse', 'HEAD');
|
||||
}
|
||||
|
||||
function branchTopologyRepository(): string
|
||||
{
|
||||
$repository = sys_get_temp_dir().DIRECTORY_SEPARATOR.'tenantpilot-branch-topology-'.bin2hex(random_bytes(8));
|
||||
mkdir($repository, 0777, true);
|
||||
|
||||
branchTopologyGit($repository, 'init', '-b', 'main');
|
||||
branchTopologyGit($repository, 'config', 'user.email', 'branch-topology@example.test');
|
||||
branchTopologyGit($repository, 'config', 'user.name', 'Branch Topology Fixture');
|
||||
branchTopologyCommit($repository, 'baseline.txt', "baseline\n", 'baseline');
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
function branchTopologyRemoveDirectory(string $directory): void
|
||||
{
|
||||
if (! is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST,
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
||||
}
|
||||
|
||||
rmdir($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{expectedBranch: string, targetCommit: string, featureCommit: string}
|
||||
*/
|
||||
function branchTopologyPrepareStream(
|
||||
string $repository,
|
||||
string $family,
|
||||
bool $remoteRef,
|
||||
bool $localRef = true,
|
||||
bool $diverged = false,
|
||||
): array {
|
||||
$expectedBranch = $family === 'platform' ? 'platform-dev' : 'website-dev';
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
|
||||
branchTopologyGit($repository, 'branch', $expectedBranch, $baseline);
|
||||
|
||||
if ($diverged) {
|
||||
branchTopologyGit($repository, 'checkout', $expectedBranch);
|
||||
$targetCommit = branchTopologyCommit(
|
||||
$repository,
|
||||
$family.'-integration.txt',
|
||||
$family." integration\n",
|
||||
$family.' integration',
|
||||
);
|
||||
} else {
|
||||
$targetCommit = $baseline;
|
||||
}
|
||||
|
||||
branchTopologyGit($repository, 'checkout', '-b', 'feat/'.$family.'-fixture', $baseline);
|
||||
$featureCommit = branchTopologyCommit(
|
||||
$repository,
|
||||
$family.'-feature.txt',
|
||||
$family." feature\n",
|
||||
$family.' feature',
|
||||
);
|
||||
|
||||
if ($remoteRef) {
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/'.$expectedBranch, $targetCommit);
|
||||
}
|
||||
|
||||
if (! $localRef) {
|
||||
branchTopologyGit($repository, 'branch', '-D', $expectedBranch);
|
||||
}
|
||||
|
||||
return compact('expectedBranch', 'targetCommit', 'featureCommit');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $arguments
|
||||
* @param array<string, string|false> $environment
|
||||
*/
|
||||
function branchTopologyResolve(
|
||||
string $repository,
|
||||
array $arguments,
|
||||
array $environment = [],
|
||||
bool $machineReadable = true,
|
||||
): Process {
|
||||
$isolatedEnvironment = [
|
||||
'TENANTATLAS_BRANCH_FAMILY' => false,
|
||||
'TENANTATLAS_WORK_CLASSIFICATION' => false,
|
||||
'TENANTATLAS_PR_TARGET' => false,
|
||||
'TENANTATLAS_ACTIVE_SPEC' => false,
|
||||
'TENANTATLAS_BASE_REF' => false,
|
||||
'TENANTATLAS_REPO_SCOPE' => false,
|
||||
'GITHUB_BASE_REF' => false,
|
||||
'GITEA_BASE_REF' => false,
|
||||
];
|
||||
|
||||
$command = [repo_path('scripts/resolve-feature-base')];
|
||||
|
||||
if ($machineReadable) {
|
||||
$command[] = '--format=kv';
|
||||
}
|
||||
|
||||
$process = new Process(
|
||||
[...$command, ...$arguments],
|
||||
$repository,
|
||||
array_replace($isolatedEnvironment, $environment),
|
||||
);
|
||||
$process->setTimeout(15);
|
||||
$process->run();
|
||||
|
||||
return $process;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
function branchTopologyKeyValues(string $output): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach (preg_split('/\R/', trim($output)) ?: [] as $line) {
|
||||
if (! str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
$values[$key] = $value;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
function branchTopologyFixture(Closure $assertions): void
|
||||
{
|
||||
$repository = branchTopologyRepository();
|
||||
|
||||
try {
|
||||
$assertions($repository);
|
||||
} finally {
|
||||
branchTopologyRemoveDirectory($repository);
|
||||
}
|
||||
}
|
||||
|
||||
it('[BS-001] selects the platform remote ref with its merge-base result', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$stream = branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['expected_branch'] ?? null)->toBe('platform-dev')
|
||||
->and($values['selected_ref'] ?? null)->toBe('origin/platform-dev')
|
||||
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-002] falls back only to the local platform ref with its merge-base result', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$stream = branchTopologyPrepareStream($repository, 'platform', remoteRef: false);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['expected_branch'] ?? null)->toBe('platform-dev')
|
||||
->and($values['selected_ref'] ?? null)->toBe('platform-dev')
|
||||
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-003] selects the website remote ref with its merge-base result', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$stream = branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('website')
|
||||
->and($values['expected_branch'] ?? null)->toBe('website-dev')
|
||||
->and($values['selected_ref'] ?? null)->toBe('origin/website-dev')
|
||||
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-004] falls back only to the local website ref with its merge-base result', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$stream = branchTopologyPrepareStream($repository, 'website', remoteRef: false);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('website')
|
||||
->and($values['expected_branch'] ?? null)->toBe('website-dev')
|
||||
->and($values['selected_ref'] ?? null)->toBe('website-dev')
|
||||
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
||||
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-005] accepts dev only for an explicitly classified promotion', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
||||
branchTopologyGit($repository, 'checkout', '-b', 'feat/promotion-fixture', $baseline);
|
||||
branchTopologyCommit($repository, 'promotion.txt', "promotion\n", 'promotion feature');
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=repository-wide',
|
||||
'--work-classification=promotion',
|
||||
'--pull-request-target=dev',
|
||||
]);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('repository-wide')
|
||||
->and($values['work_classification'] ?? null)->toBe('promotion')
|
||||
->and($values['expected_branch'] ?? null)->toBe('dev')
|
||||
->and($values['selected_ref'] ?? null)->toBe('origin/dev')
|
||||
->and($values['target_commit'] ?? null)->toBe($baseline)
|
||||
->and($values['merge_base'] ?? null)->toBe($baseline)
|
||||
->and($values['base_range'] ?? null)->toBe($baseline.'...HEAD');
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-006] fails closed when platform family refs are missing', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
$error = $process->getErrorOutput();
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($error)->toContain('status=failed', 'reason=missing-family-ref')
|
||||
->and($error)->toContain(
|
||||
'branch_family=platform',
|
||||
'expected_branch=platform-dev',
|
||||
'attempted_refs=origin/platform-dev,platform-dev',
|
||||
'guidance=Declare a consistent branch family',
|
||||
'diff_ran=false',
|
||||
)
|
||||
->and($error)->not->toContain(
|
||||
'selected_ref=origin/dev',
|
||||
'selected_ref=dev',
|
||||
'HEAD~1',
|
||||
'merge_base=',
|
||||
'base_range=',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-007] fails closed when website family refs are missing', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'platform-dev', $baseline);
|
||||
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
||||
$error = $process->getErrorOutput();
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($error)->toContain(
|
||||
'reason=missing-family-ref',
|
||||
'branch_family=website',
|
||||
'expected_branch=website-dev',
|
||||
'attempted_refs=origin/website-dev,website-dev',
|
||||
'guidance=Declare a consistent branch family',
|
||||
'diff_ran=false',
|
||||
)
|
||||
->and($error)->not->toContain(
|
||||
'selected_ref=origin/dev',
|
||||
'selected_ref=dev',
|
||||
'HEAD~1',
|
||||
'merge_base=',
|
||||
'base_range=',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-008] returns only the platform feature path for diverged history', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true, diverged: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
$changed = branchTopologyGit($repository, 'diff', '--name-only', $values['base_range']);
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($changed)->toBe('platform-feature.txt')
|
||||
->and($values['merge_base'] ?? null)->not->toBe($values['target_commit'] ?? null);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-009] returns only the website feature path for diverged history', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'website', remoteRef: true, diverged: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
$changed = branchTopologyGit($repository, 'diff', '--name-only', $values['base_range']);
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($changed)->toBe('website-feature.txt')
|
||||
->and($values['merge_base'] ?? null)->not->toBe($values['target_commit'] ?? null);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-010] rejects an explicit platform family plus website PR target', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=platform',
|
||||
'--pull-request-target=website-dev',
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=branch-family-conflict',
|
||||
'explicit=platform',
|
||||
'pull_request_target=website',
|
||||
'guidance=Declare a consistent branch family',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-011] rejects conflicting branch-name and repository-scope signals', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$specPath = $repository.'/spec.md';
|
||||
file_put_contents($specPath, "# Ambiguous fixture\n\nNo branch metadata.\n");
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-name=feat/platform-fixture',
|
||||
'--repo-scope=website',
|
||||
'--spec='.$specPath,
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=ambiguous-branch-family',
|
||||
'branch_name=feat/platform-fixture',
|
||||
'repo_scope=website',
|
||||
'spec_path='.$specPath,
|
||||
'guidance=Declare a consistent branch family',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('[BS-012] rejects an explicit ref from the wrong family before merge-base work', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'platform-dev', $baseline);
|
||||
branchTopologyGit($repository, 'branch', 'website-dev', $baseline);
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/website-dev', $baseline);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=platform',
|
||||
'--ref=origin/website-dev',
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=wrong-family-ref',
|
||||
'expected_branch=platform-dev',
|
||||
'explicit_ref=origin/website-dev',
|
||||
'guidance=Declare a consistent branch family',
|
||||
'diff_ran=false',
|
||||
)
|
||||
->and($process->getErrorOutput())->not->toContain(
|
||||
'target_commit=',
|
||||
'merge_base=',
|
||||
'base_range=',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('proves Stage 1 priority, diagnostics, ancestry failure, and executable portability', function (): void {
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, ['--pull-request-target=website-dev']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('website')
|
||||
->and($values['family_source'] ?? null)->toBe('pull-request-target');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$specPath = $repository.'/spec.md';
|
||||
file_put_contents($specPath, "**Feature Base**: platform-dev\n**Normal Merge Target**: platform-dev\n");
|
||||
$process = branchTopologyResolve($repository, ['--spec='.$specPath]);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['family_source'] ?? null)->toBe('spec-metadata');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-name=feat/platform-policy-fixture',
|
||||
'--repo-scope=platform',
|
||||
]);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['family_source'] ?? null)->toBe('branch-and-scope');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values)->toHaveKeys([
|
||||
'status',
|
||||
'branch_family',
|
||||
'work_classification',
|
||||
'family_source',
|
||||
'expected_branch',
|
||||
'selected_ref',
|
||||
'target_commit',
|
||||
'merge_base',
|
||||
'base_range',
|
||||
])
|
||||
->and($values['status'])->toBe('resolved')
|
||||
->and($values['target_commit'])->toMatch('/^[0-9a-f]{40}$/')
|
||||
->and($values['merge_base'])->toMatch('/^[0-9a-f]{40}$/')
|
||||
->and($values['base_range'])->toBe($values['merge_base'].'...HEAD');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
||||
$process = branchTopologyResolve($repository, ['--pull-request-target=dev']);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=explicit-promotion-classification-required',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=unknown']);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=invalid-branch-family',
|
||||
'explicit=unknown',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$specPath = $repository.'/spec.md';
|
||||
file_put_contents($specPath, "**Feature Base**: platform-dev\n**Normal Merge Target**: platform-dev\n");
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--pull-request-target=website-dev',
|
||||
'--spec='.$specPath,
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=branch-family-conflict',
|
||||
'selected=website',
|
||||
'spec_metadata=platform',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, ['--branch-name=feat/platform-name-only']);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=ambiguous-branch-family',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve(
|
||||
$repository,
|
||||
[],
|
||||
['TENANTATLAS_BRANCH_FAMILY' => 'platform'],
|
||||
);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['family_source'] ?? null)->toBe('explicit');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
||||
$process = branchTopologyResolve(
|
||||
$repository,
|
||||
[],
|
||||
['GITHUB_BASE_REF' => 'website-dev'],
|
||||
);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('website')
|
||||
->and($values['family_source'] ?? null)->toBe('pull-request-target');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=platform',
|
||||
'--ref=platform-dev',
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and(branchTopologyKeyValues($process->getOutput())['selected_ref'] ?? null)->toBe('platform-dev');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve(
|
||||
$repository,
|
||||
['--pull-request-target=platform-dev'],
|
||||
['GITHUB_BASE_REF' => 'refs/heads/platform-dev'],
|
||||
);
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and(branchTopologyKeyValues($process->getOutput())['branch_family'] ?? null)->toBe('platform');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=platform',
|
||||
'--branch-name=feat/website-stale-hint',
|
||||
'--repo-scope=website',
|
||||
]);
|
||||
$values = branchTopologyKeyValues($process->getOutput());
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($values['branch_family'] ?? null)->toBe('platform')
|
||||
->and($values['family_source'] ?? null)->toBe('explicit');
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
||||
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
||||
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
||||
$process = branchTopologyResolve($repository, [
|
||||
'--branch-family=repository-wide',
|
||||
'--work-classification=promotion',
|
||||
]);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=explicit-target-required',
|
||||
'work_classification=promotion',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
||||
$process = branchTopologyResolve(
|
||||
$repository,
|
||||
['--branch-family=platform'],
|
||||
[],
|
||||
machineReadable: false,
|
||||
);
|
||||
expect($process->getExitCode())->toBe(0)
|
||||
->and($process->getOutput())->toContain(
|
||||
'Feature base resolved.',
|
||||
'Branch family: platform',
|
||||
'Expected integration branch: platform-dev',
|
||||
'Selected ref: origin/platform-dev',
|
||||
'Target commit:',
|
||||
'Merge base:',
|
||||
'Base range:',
|
||||
);
|
||||
});
|
||||
|
||||
branchTopologyFixture(function (string $repository): void {
|
||||
branchTopologyGit($repository, 'checkout', '--orphan', 'platform-dev');
|
||||
branchTopologyGit($repository, 'rm', '-rf', '.');
|
||||
branchTopologyCommit($repository, 'unrelated.txt', "unrelated\n", 'unrelated root');
|
||||
branchTopologyGit($repository, 'checkout', 'main');
|
||||
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
||||
expect($process->getExitCode())->toBe(2)
|
||||
->and($process->getErrorOutput())->toContain(
|
||||
'reason=no-common-merge-base',
|
||||
'selected_ref=platform-dev',
|
||||
'diff_ran=false',
|
||||
);
|
||||
});
|
||||
|
||||
$resolver = repo_path('scripts/resolve-feature-base');
|
||||
$syntax = new Process(['bash', '-n', $resolver], repo_root());
|
||||
$syntax->run();
|
||||
expect(is_executable($resolver))->toBeTrue()
|
||||
->and($syntax->getExitCode())->toBe(0, $syntax->getErrorOutput());
|
||||
});
|
||||
@ -16,6 +16,12 @@
|
||||
->and($workflowContents)->toContain('actions: read')
|
||||
->and($workflowContents)->toContain('contents: read')
|
||||
->and($workflowContents)->toContain('opened', 'reopened', 'synchronize')
|
||||
->and($workflowContents)->toContain('fetch-depth: 0')
|
||||
->and($workflowContents)->toContain('TENANTATLAS_PR_TARGET: ${{ github.base_ref }}')
|
||||
->and($workflowContents)->toContain('if [[ -z "${TENANTATLAS_PR_TARGET}" ]]')
|
||||
->and($workflowContents)->toContain('git fetch origin "${TENANTATLAS_PR_TARGET}:refs/remotes/origin/${TENANTATLAS_PR_TARGET}"')
|
||||
->and($workflowContents)->toContain('bash scripts/check-ui-productization-coverage "origin/${TENANTATLAS_PR_TARGET}" --pull-request-target="${TENANTATLAS_PR_TARGET}"')
|
||||
->and($workflowContents)->not->toContain('git fetch origin dev', 'scripts/check-ui-productization-coverage origin/dev')
|
||||
->and($workflowContents)->toContain('./scripts/platform-test-lane fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request')
|
||||
->and($workflowContents)->toContain('TENANTATLAS_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}')
|
||||
->and($workflowContents)->toContain('./scripts/platform-test-report fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request --fetch-latest-history')
|
||||
|
||||
@ -5,6 +5,30 @@
|
||||
use Tests\Support\TestLaneBudget;
|
||||
use Tests\Support\TestLaneReport;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $budgetOutcome
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec439SuccessfulExecutionWithBudget(array $budgetOutcome): array
|
||||
{
|
||||
return TestLaneReport::deriveFinalResult(
|
||||
execution: TestLaneReport::executionResult(0),
|
||||
evidence: [
|
||||
'path' => '/tmp/spec439-budget.junit.xml',
|
||||
'exists' => true,
|
||||
'parseValid' => true,
|
||||
'testCount' => 1,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
'status' => 'passed',
|
||||
],
|
||||
reportGeneration: ['status' => 'succeeded'],
|
||||
artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []],
|
||||
budgetOutcome: $budgetOutcome,
|
||||
);
|
||||
}
|
||||
|
||||
it('classifies wrapper or manifest drift before lower-level lane failures', function (): void {
|
||||
$primaryFailureClassId = TestLaneReport::classifyPrimaryFailure(
|
||||
exitCode: 1,
|
||||
@ -13,17 +37,37 @@
|
||||
entryPointResolved: false,
|
||||
workflowLaneMatched: false,
|
||||
);
|
||||
$finalResult = TestLaneReport::deriveFinalResult(
|
||||
execution: TestLaneReport::executionResult(0),
|
||||
evidence: [
|
||||
'status' => 'passed',
|
||||
'testCount' => 1,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
],
|
||||
reportGeneration: ['status' => 'succeeded'],
|
||||
artifactPublicationStatus: ['complete' => true],
|
||||
entryPointResolved: false,
|
||||
workflowLaneMatched: false,
|
||||
);
|
||||
|
||||
expect($primaryFailureClassId)->toBe('wrapper-failure');
|
||||
expect($primaryFailureClassId)->toBe('wrapper-failure')
|
||||
->and($finalResult['exitCode'])->toBe(1)
|
||||
->and($finalResult['failureContextIds'])->toContain('wrapper-failure');
|
||||
});
|
||||
|
||||
it('keeps confidence budget overruns visible without converting them into blocking failures', function (): void {
|
||||
it('[EF-004] keeps confidence budget overruns visible without converting them into blocking failures', function (): void {
|
||||
$budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 481.0);
|
||||
$finalResult = spec439SuccessfulExecutionWithBudget($budgetOutcome);
|
||||
$summary = TestLaneReport::buildCiSummary(
|
||||
report: [
|
||||
'laneId' => 'confidence',
|
||||
'budgetStatus' => 'within-budget',
|
||||
'ciContext' => ['workflowId' => 'main-confidence'],
|
||||
'execution' => TestLaneReport::executionResult(0),
|
||||
'evidence' => ['status' => 'passed'],
|
||||
'finalResult' => $finalResult,
|
||||
],
|
||||
exitCode: 0,
|
||||
budgetOutcome: $budgetOutcome,
|
||||
@ -31,17 +75,26 @@
|
||||
);
|
||||
|
||||
expect($budgetOutcome['budgetStatus'])->toBe('over-budget')
|
||||
->and($finalResult['exitCode'])->toBe(0)
|
||||
->and($finalResult['status'])->toBe('passed')
|
||||
->and($finalResult['blockingStatus'])->toBe('non-blocking-warning')
|
||||
->and($finalResult['advisoryContextIds'])->toContain('budget-advisory-warning')
|
||||
->and($summary['primaryFailureClassId'])->toBe('budget-breach')
|
||||
->and($summary['blockingStatus'])->toBe('non-blocking-warning');
|
||||
->and($summary['blockingStatus'])->toBe('non-blocking-warning')
|
||||
->and($summary['finalExitCode'])->toBe(0);
|
||||
});
|
||||
|
||||
it('treats mature fast-feedback budget overruns as blocking when they exceed the CI tolerance', function (): void {
|
||||
it('[EF-003] treats mature fast-feedback budget overruns as blocking with a non-zero final exit', function (): void {
|
||||
$budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 216.0);
|
||||
$finalResult = spec439SuccessfulExecutionWithBudget($budgetOutcome);
|
||||
$summary = TestLaneReport::buildCiSummary(
|
||||
report: [
|
||||
'laneId' => 'fast-feedback',
|
||||
'budgetStatus' => 'within-budget',
|
||||
'ciContext' => ['workflowId' => 'pr-fast-feedback'],
|
||||
'execution' => TestLaneReport::executionResult(0),
|
||||
'evidence' => ['status' => 'passed'],
|
||||
'finalResult' => $finalResult,
|
||||
],
|
||||
exitCode: 0,
|
||||
budgetOutcome: $budgetOutcome,
|
||||
@ -49,27 +102,52 @@
|
||||
);
|
||||
|
||||
expect($budgetOutcome['budgetStatus'])->toBe('over-budget')
|
||||
->and($finalResult['exitCode'])->toBe(1)
|
||||
->and($finalResult['status'])->toBe('failed')
|
||||
->and($finalResult['failureContextIds'])->toContain('blocking-budget-breach')
|
||||
->and($summary['primaryFailureClassId'])->toBe('budget-breach')
|
||||
->and($summary['blockingStatus'])->toBe('blocking');
|
||||
->and($summary['blockingStatus'])->toBe('blocking')
|
||||
->and($summary['finalExitCode'])->toBe(1);
|
||||
});
|
||||
|
||||
it('classifies incomplete artifact bundles independently from test and budget status', function (): void {
|
||||
$artifactPublicationStatus = [
|
||||
'complete' => false,
|
||||
'publishedArtifacts' => [],
|
||||
'missingRequiredArtifacts' => ['report.json'],
|
||||
];
|
||||
$finalResult = TestLaneReport::deriveFinalResult(
|
||||
execution: TestLaneReport::executionResult(0),
|
||||
evidence: [
|
||||
'status' => 'passed',
|
||||
'testCount' => 1,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
],
|
||||
reportGeneration: ['status' => 'succeeded'],
|
||||
artifactPublicationStatus: $artifactPublicationStatus,
|
||||
);
|
||||
$summary = TestLaneReport::buildCiSummary(
|
||||
report: [
|
||||
'laneId' => 'fast-feedback',
|
||||
'budgetStatus' => 'within-budget',
|
||||
'ciContext' => ['workflowId' => 'pr-fast-feedback'],
|
||||
'execution' => TestLaneReport::executionResult(0),
|
||||
'evidence' => ['status' => 'passed'],
|
||||
'finalResult' => $finalResult,
|
||||
],
|
||||
exitCode: 0,
|
||||
budgetOutcome: null,
|
||||
artifactPublicationStatus: [
|
||||
'complete' => false,
|
||||
'publishedArtifacts' => [],
|
||||
],
|
||||
artifactPublicationStatus: $artifactPublicationStatus,
|
||||
);
|
||||
|
||||
expect($summary['primaryFailureClassId'])->toBe('artifact-publication-failure')
|
||||
->and($summary['blockingStatus'])->toBe('blocking');
|
||||
->and($summary['blockingStatus'])->toBe('blocking')
|
||||
->and($finalResult['status'])->toBe('failed')
|
||||
->and($finalResult['exitCode'])->toBe(1)
|
||||
->and($finalResult['failureContextIds'])->toContain('artifact-publication-failure')
|
||||
->and($summary['finalExitCode'])->toBe(1);
|
||||
});
|
||||
|
||||
it('surfaces classification-only scope boundaries for heavy governance and browser reports', function (): void {
|
||||
@ -94,4 +172,4 @@
|
||||
|
||||
expect($heavySummary['scopeBoundaryNote'])->toContain('full-suite repair ownership')
|
||||
->and($browserSummary['scopeBoundaryNote'])->toContain('classification-only');
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function spec439ConstitutionContents(): string
|
||||
{
|
||||
$path = repo_path('.specify/memory/constitution.md');
|
||||
|
||||
expect(is_file($path))->toBeTrue();
|
||||
|
||||
return (string) file_get_contents($path);
|
||||
}
|
||||
|
||||
function spec439ConstitutionUnrelatedBody(string $contents): string
|
||||
{
|
||||
$withoutSyncImpact = preg_replace(
|
||||
'/\A<!--\RSync Impact Report.*?-->\R\R/s',
|
||||
'',
|
||||
$contents,
|
||||
);
|
||||
$withoutBranchAmendment = preg_replace(
|
||||
'/### Spec-First Workflow\R.*?(?=\R## Quality Gates)/s',
|
||||
"### Spec-First Workflow\n[BRANCH-TOPOLOGY-AMENDMENT]\n",
|
||||
$withoutSyncImpact,
|
||||
);
|
||||
$withoutAmendmentMetadata = preg_replace(
|
||||
'/^\*\*Version\*\*:.*$/m',
|
||||
'**Version**: [AMENDMENT-METADATA]',
|
||||
$withoutBranchAmendment,
|
||||
);
|
||||
|
||||
if (! is_string($withoutAmendmentMetadata)) {
|
||||
throw new RuntimeException('Unable to normalize Constitution amendment boundaries.');
|
||||
}
|
||||
|
||||
return $withoutAmendmentMetadata;
|
||||
}
|
||||
|
||||
it('defines the three canonical branch roles and fail-closed scope routing', function (): void {
|
||||
$contents = spec439ConstitutionContents();
|
||||
|
||||
expect($contents)
|
||||
->toContain(
|
||||
'`dev` is the repository-wide integration and promotion branch',
|
||||
'`platform-dev` is the integration branch for TenantPilot platform work',
|
||||
'`website-dev` is the integration branch for website work',
|
||||
'Ordinary platform work MUST branch from, target, and diff against `platform-dev`',
|
||||
'ordinary website work MUST branch from, target, and diff against `website-dev`',
|
||||
'Cross-stream integration, promotion, and repository-governance work MUST be explicitly classified',
|
||||
'Such work MAY target `dev`; the existence of `dev` alone MUST NOT imply that target.',
|
||||
'Ambiguous or missing branch-family/ref resolution MUST fail closed.',
|
||||
'`origin/dev`, the wrong branch family, `HEAD~1`, or an arbitrary prior commit',
|
||||
)
|
||||
->not->toContain('- New work branches from `dev` using `feat/<NNN>-<slug>`');
|
||||
|
||||
expect(substr_count($contents, '### Spec-First Workflow'))->toBe(1);
|
||||
});
|
||||
|
||||
it('keeps the major amendment metadata and Sync Impact report coherent', function (): void {
|
||||
$contents = spec439ConstitutionContents();
|
||||
|
||||
expect($contents)
|
||||
->toContain(
|
||||
'- Version change: 2.16.0 -> 3.0.0',
|
||||
'MAJOR because a normative universal branch rule is redefined',
|
||||
'- Added sections: None',
|
||||
'- Removed sections: None',
|
||||
'✅ `Agents.md`: directly conflicting universal `dev` instructions were',
|
||||
'✅ Specs 437 and 438: historical/completed or deferred context remains',
|
||||
'**Version**: 3.0.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-07-11',
|
||||
)
|
||||
->not->toContain('⏳ `Agents.md`');
|
||||
|
||||
expect(substr_count($contents, 'Sync Impact Report'))->toBe(1)
|
||||
->and(substr_count($contents, '**Version**:'))->toBe(1);
|
||||
});
|
||||
|
||||
it('locks all Constitution content outside the authorized amendment boundaries', function (): void {
|
||||
$normalized = spec439ConstitutionUnrelatedBody(spec439ConstitutionContents());
|
||||
|
||||
expect(hash('sha256', $normalized))
|
||||
->toBe('9ea4019798f1854e7b513a8fe0bce2529a19207d0b16e81a08582e6b661b8e74');
|
||||
});
|
||||
|
||||
it('keeps authoritative repository branch instructions synchronized', function (): void {
|
||||
$instructions = (string) file_get_contents(repo_path('Agents.md'));
|
||||
|
||||
expect($instructions)
|
||||
->toContain(
|
||||
'`dev`: repository-weite Integration und Promotion',
|
||||
'`platform-dev`: Integration für TenantPilot-Plattformarbeit',
|
||||
'`website-dev`: Integration für Website-Arbeit',
|
||||
'Normale Plattformarbeit startet von und zielt auf `platform-dev`',
|
||||
'Website-Arbeit startet von und zielt auf `website-dev`',
|
||||
'Cross-Stream-, Promotion- und Repository-Governance-Arbeit muss ausdrücklich',
|
||||
'`dev` darf dabei ein',
|
||||
'bewusstes Integrations-/Promotion-Ziel sein, aber kein stiller Fallback.',
|
||||
'Für normale Plattformarbeit ist das `origin/platform-dev`',
|
||||
'Website-Arbeit `origin/website-dev`',
|
||||
'`origin/dev` ist nur für ausdrücklich als',
|
||||
)
|
||||
->not->toContain(
|
||||
'- Default / Integrations-Branch: `dev`',
|
||||
'Neue Arbeit läuft über Feature-Branches von `dev`',
|
||||
'PR/MR: `feat/...` → `dev`',
|
||||
"git merge origin/dev\n",
|
||||
);
|
||||
});
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\Support\TestLaneManifest;
|
||||
use Tests\Support\TestLaneReport;
|
||||
|
||||
@ -116,35 +117,16 @@ function heavyGovernanceSyntheticHotspots(): array
|
||||
});
|
||||
|
||||
it('stages deterministic CI artifact bundles from the canonical lane outputs', function (): void {
|
||||
$artifactDirectory = 'storage/logs/test-lanes/contract-stage-test';
|
||||
$stagingDirectory = base_path('storage/logs/test-lanes/contract-stage-test/staged');
|
||||
$report = TestLaneReport::buildReport(
|
||||
laneId: 'fast-feedback',
|
||||
wallClockSeconds: 182.4,
|
||||
slowestEntries: [],
|
||||
durationsByFile: [],
|
||||
artifactDirectory: $artifactDirectory,
|
||||
ciContext: [
|
||||
'workflowId' => 'pr-fast-feedback',
|
||||
'triggerClass' => 'pull-request',
|
||||
'entryPointResolved' => true,
|
||||
'workflowLaneMatched' => true,
|
||||
],
|
||||
);
|
||||
|
||||
TestLaneReport::writeArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
report: $report,
|
||||
profileOutput: null,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
exitCode: 0,
|
||||
);
|
||||
|
||||
$artifactPaths = TestLaneReport::artifactPaths('fast-feedback', $artifactDirectory);
|
||||
$artifactDirectory = 'storage/logs/test-lanes/contract-stage-'.bin2hex(random_bytes(6));
|
||||
$stagingDirectory = base_path($artifactDirectory.'/staged');
|
||||
$artifactPaths = TestLaneReport::artifactPaths('fast-feedback', $artifactDirectory);
|
||||
$artifactRoot = base_path($artifactDirectory);
|
||||
|
||||
try {
|
||||
File::ensureDirectoryExists($artifactRoot);
|
||||
file_put_contents(
|
||||
TestLaneManifest::absolutePath($artifactPaths['junit']),
|
||||
<<<'XML'
|
||||
TestLaneManifest::absolutePath($artifactPaths['junit']),
|
||||
<<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="fast-feedback" tests="1" assertions="1" errors="0" failures="0" skipped="0" time="0.1">
|
||||
@ -154,25 +136,99 @@ function heavyGovernanceSyntheticHotspots(): array
|
||||
XML,
|
||||
);
|
||||
|
||||
$stagingResult = TestLaneReport::stageArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
stagingDirectory: $stagingDirectory,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
expect($stagingResult['complete'])->toBeTrue()
|
||||
->and(collect($stagingResult['stagedArtifacts'])->pluck('artifactType')->all())
|
||||
->toEqualCanonicalizing(['summary.md', 'budget.json', 'report.json', 'junit.xml', 'trend-history.json'])
|
||||
->and(collect($stagingResult['stagedArtifacts'])->pluck('relativePath')->all())
|
||||
->toContain(
|
||||
$stagingDirectory.'/fast-feedback.summary.md',
|
||||
$stagingDirectory.'/fast-feedback.budget.json',
|
||||
$stagingDirectory.'/fast-feedback.report.json',
|
||||
$stagingDirectory.'/fast-feedback.junit.xml',
|
||||
$stagingDirectory.'/fast-feedback.trend-history.json',
|
||||
$report = TestLaneReport::buildReport(
|
||||
laneId: 'fast-feedback',
|
||||
wallClockSeconds: 182.4,
|
||||
slowestEntries: [],
|
||||
durationsByFile: [],
|
||||
artifactDirectory: $artifactDirectory,
|
||||
ciContext: [
|
||||
'workflowId' => 'pr-fast-feedback',
|
||||
'triggerClass' => 'pull-request',
|
||||
'entryPointResolved' => true,
|
||||
'workflowLaneMatched' => true,
|
||||
],
|
||||
);
|
||||
|
||||
TestLaneReport::writeArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
report: $report,
|
||||
profileOutput: null,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
exitCode: 0,
|
||||
);
|
||||
|
||||
$stagingResult = TestLaneReport::stageArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
stagingDirectory: $stagingDirectory,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
$stagedReport = json_decode(
|
||||
(string) file_get_contents($stagingDirectory.'/fast-feedback.report.json'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
expect($stagingResult['complete'])->toBeTrue()
|
||||
->and(collect($stagingResult['stagedArtifacts'])->pluck('artifactType')->all())
|
||||
->toEqualCanonicalizing(['summary.md', 'budget.json', 'report.json', 'junit.xml', 'trend-history.json'])
|
||||
->and(collect($stagingResult['stagedArtifacts'])->pluck('relativePath')->all())
|
||||
->toContain(
|
||||
$stagingDirectory.'/fast-feedback.summary.md',
|
||||
$stagingDirectory.'/fast-feedback.budget.json',
|
||||
$stagingDirectory.'/fast-feedback.report.json',
|
||||
$stagingDirectory.'/fast-feedback.junit.xml',
|
||||
$stagingDirectory.'/fast-feedback.trend-history.json',
|
||||
)
|
||||
->and($stagedReport['evidence']['status'] ?? null)->toBe('passed')
|
||||
->and($stagedReport['finalResult']['exitCode'] ?? null)->toBe(0);
|
||||
} finally {
|
||||
File::deleteDirectory($artifactRoot);
|
||||
}
|
||||
});
|
||||
|
||||
it('fails artifact publication when a required file cannot be copied into staging', function (): void {
|
||||
$artifactDirectory = 'storage/logs/test-lanes/contract-stage-copy-failure-'.bin2hex(random_bytes(6));
|
||||
$stagingDirectory = base_path($artifactDirectory.'/staged');
|
||||
$artifactPaths = TestLaneReport::artifactPaths('fast-feedback', $artifactDirectory);
|
||||
$artifactRoot = base_path($artifactDirectory);
|
||||
|
||||
try {
|
||||
File::ensureDirectoryExists($artifactRoot);
|
||||
file_put_contents(
|
||||
TestLaneManifest::absolutePath($artifactPaths['junit']),
|
||||
'<?xml version="1.0"?><testsuites><testsuite name="fast-feedback"><testcase name="passes" file="passes.php" time="0.1"/></testsuite></testsuites>',
|
||||
);
|
||||
TestLaneReport::writeArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
report: TestLaneReport::buildReport(
|
||||
laneId: 'fast-feedback',
|
||||
wallClockSeconds: 1.0,
|
||||
slowestEntries: [],
|
||||
durationsByFile: [],
|
||||
artifactDirectory: $artifactDirectory,
|
||||
),
|
||||
artifactDirectory: $artifactDirectory,
|
||||
exitCode: 0,
|
||||
);
|
||||
|
||||
File::ensureDirectoryExists($stagingDirectory.'/fast-feedback.report.json');
|
||||
|
||||
$stagingResult = TestLaneReport::stageArtifacts(
|
||||
laneId: 'fast-feedback',
|
||||
stagingDirectory: $stagingDirectory,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
expect($stagingResult['complete'])->toBeFalse()
|
||||
->and($stagingResult['primaryFailureClassId'])->toBe('artifact-publication-failure')
|
||||
->and($stagingResult['missingRequiredArtifacts'])->toContain('report.json')
|
||||
->and(collect($stagingResult['stagedArtifacts'])->pluck('artifactType')->all())->not->toContain('report.json');
|
||||
} finally {
|
||||
File::deleteDirectory($artifactRoot);
|
||||
}
|
||||
});
|
||||
|
||||
it('publishes the shared fixture slimming comparison only for the governed standard lanes', function (): void {
|
||||
$fastFeedback = TestLaneReport::buildReport(
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
|
||||
it('keeps the host-side lane runner and report scripts checked in at repo root', function (): void {
|
||||
expect(file_exists(repo_path('scripts/platform-test-lane')))->toBeTrue()
|
||||
->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue()
|
||||
->and(file_exists(repo_path('scripts/platform-test-artifacts')))->toBeTrue();
|
||||
->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue()
|
||||
->and(file_exists(repo_path('scripts/platform-test-artifacts')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('passes artifact staging inputs through php argv for sail execution', function (): void {
|
||||
@ -70,12 +70,63 @@
|
||||
|
||||
expect($laneRunner)
|
||||
->toContain('if [[ ${#remaining_args[@]} -gt 0 ]]; then')
|
||||
->and($laneRunner)->toContain('./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "${remaining_args[@]}"')
|
||||
->and($laneRunner)->toContain('./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"')
|
||||
->and($laneRunner)->toContain('sail_composer=(')
|
||||
->and($laneRunner)->toContain('"TENANTATLAS_CI_WORKFLOW_ID=${WORKFLOW_ID}"')
|
||||
->and($laneRunner)->toContain('"TENANTATLAS_CI_TRIGGER_CLASS=${TRIGGER_CLASS}"')
|
||||
->and($laneRunner)->toContain('"${sail_composer[@]}" -- "${remaining_args[@]}"')
|
||||
->and($laneRunner)->toContain('"${sail_composer[@]}"')
|
||||
->and($laneRunner)->not->toContain('./vendor/bin/sail composer run')
|
||||
->and($laneRunner)->toContain('--workflow-id=')
|
||||
->and($laneRunner)->toContain('--trigger-class=');
|
||||
});
|
||||
|
||||
it('propagates manifest lane and report result codes without duplicating final-result policy', function (): void {
|
||||
$laneRunner = (string) file_get_contents(repo_path('scripts/platform-test-lane'));
|
||||
$reportRunner = (string) file_get_contents(repo_path('scripts/platform-test-report'));
|
||||
$composerContents = (string) file_get_contents(base_path('composer.json'));
|
||||
$composer = json_decode($composerContents, true, 512, JSON_THROW_ON_ERROR);
|
||||
$scripts = $composer['scripts'] ?? [];
|
||||
|
||||
expect($laneRunner)->toContain('set -euo pipefail')
|
||||
->and($laneRunner)->toContain('./vendor/bin/sail', 'run', 'env', 'composer', 'run', '--timeout=0')
|
||||
->and($laneRunner)->toContain('"TENANTATLAS_CI_WORKFLOW_ID=${WORKFLOW_ID}"')
|
||||
->and($laneRunner)->toContain('"TENANTATLAS_CI_TRIGGER_CLASS=${TRIGGER_CLASS}"')
|
||||
->and($laneRunner)->not->toContain('"${sail_composer[@]}" || true')
|
||||
->and($reportRunner)->toContain('set -euo pipefail')
|
||||
->and($reportRunner)->toContain('./vendor/bin/sail', 'run', 'env', 'composer', 'run', '--timeout=0')
|
||||
->and($reportRunner)->toContain('"TENANTATLAS_CI_WORKFLOW_ID=${WORKFLOW_ID}"')
|
||||
->and($reportRunner)->toContain('"TENANTATLAS_CI_TRIGGER_CLASS=${TRIGGER_CLASS}"')
|
||||
->and($reportRunner)->not->toContain('"${sail_composer[@]}" || true');
|
||||
|
||||
foreach ([
|
||||
'test' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('fast-feedback'));",
|
||||
'test:fast' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('fast-feedback'));",
|
||||
'test:confidence' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('confidence'));",
|
||||
'test:browser' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('browser'));",
|
||||
'test:heavy' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('heavy-governance'));",
|
||||
'test:profile' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('profiling'));",
|
||||
'test:junit' => "exit(\\Tests\\Support\\TestLaneManifest::runLane('junit'));",
|
||||
'test:report' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('fast-feedback', 'shared-test-fixture-slimming'));",
|
||||
'test:report:confidence' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('confidence', 'shared-test-fixture-slimming'));",
|
||||
'test:report:browser' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('browser', ''));",
|
||||
'test:report:heavy' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('heavy-governance', ''));",
|
||||
'test:report:profile' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('profiling', ''));",
|
||||
'test:report:junit' => "exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('junit', ''));",
|
||||
] as $scriptName => $expectedExitCall) {
|
||||
expect($scripts)->toHaveKey($scriptName)
|
||||
->and(implode("\n", (array) $scripts[$scriptName]))->toContain($expectedExitCall);
|
||||
}
|
||||
|
||||
foreach ([$laneRunner, $reportRunner, $composerContents] as $entryPointContents) {
|
||||
expect($entryPointContents)
|
||||
->not->toContain('TestLaneBudget::')
|
||||
->not->toContain('enforcementMode')
|
||||
->not->toContain('hard-fail')
|
||||
->not->toContain('soft-warn')
|
||||
->not->toContain('trend-only');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps CI workflow validation and wrong-lane drift detection in the checked-in manifest', function (): void {
|
||||
$validExecution = TestLaneManifest::validateWorkflowExecution('pr-fast-feedback', 'fast-feedback');
|
||||
$wrongLaneExecution = TestLaneManifest::validateWorkflowExecution('pr-fast-feedback', 'confidence');
|
||||
@ -107,6 +158,36 @@
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed when an explicit trigger contradicts its workflow profile', function (): void {
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
|
||||
try {
|
||||
foreach ([
|
||||
['workflowId' => 'heavy-governance-manual', 'laneId' => 'heavy-governance', 'triggerClass' => 'manual', 'valid' => true],
|
||||
['workflowId' => 'browser-scheduled', 'laneId' => 'browser', 'triggerClass' => 'scheduled', 'valid' => true],
|
||||
['workflowId' => 'heavy-governance-manual', 'laneId' => 'heavy-governance', 'triggerClass' => 'scheduled', 'valid' => false],
|
||||
['workflowId' => 'browser-scheduled', 'laneId' => 'browser', 'triggerClass' => 'manual', 'valid' => false],
|
||||
] as $fixture) {
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID='.$fixture['workflowId']);
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$fixture['triggerClass']);
|
||||
|
||||
$context = TestLaneManifest::currentCiContext($fixture['laneId']);
|
||||
|
||||
expect($context['entryPointResolved'])->toBe($fixture['valid'])
|
||||
->and($context['workflowLaneMatched'])->toBeTrue();
|
||||
|
||||
if (! $fixture['valid']) {
|
||||
expect($context['primaryFailureClassId'] ?? null)->toBe('wrapper-failure')
|
||||
->and($context['unresolvedEntryPoints'] ?? [])->toContain('trigger-class');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
putenv($originalWorkflowId === false ? 'TENANTATLAS_CI_WORKFLOW_ID' : sprintf('TENANTATLAS_CI_WORKFLOW_ID=%s', $originalWorkflowId));
|
||||
putenv($originalTriggerClass === false ? 'TENANTATLAS_CI_TRIGGER_CLASS' : sprintf('TENANTATLAS_CI_TRIGGER_CLASS=%s', $originalTriggerClass));
|
||||
}
|
||||
});
|
||||
|
||||
it('routes the foundational lane commands through stable artisan arguments', function (): void {
|
||||
$fastFeedbackConfig = TestLaneManifest::laneConfigurationPath('fast-feedback');
|
||||
$fastFeedbackContents = (string) file_get_contents(TestLaneManifest::absolutePath($fastFeedbackConfig));
|
||||
|
||||
@ -65,12 +65,15 @@ static function (float $seconds, int $index) use ($templateRecord): array {
|
||||
it('classifies healthy, budget-near, trending-worse, regressed, and unstable runtime states', function (): void {
|
||||
$healthy = classifiedTrendReport('fast-feedback', 'healthy', [176.1, 175.6, 176.2, 175.4, 176.0], 176.4);
|
||||
$budgetNear = classifiedTrendReport('confidence', 'budget-near', [433.0, 430.0, 427.0, 424.0, 420.0], 438.5);
|
||||
$stableBudgetBreach = classifiedTrendReport('confidence', 'stable-budget-breach', [459.0, 458.0, 457.0, 456.0, 455.0], 460.0);
|
||||
$trendingWorse = classifiedTrendReport('fast-feedback', 'trending-worse', [175.0, 150.0, 125.0, 100.0, 75.0], 195.0);
|
||||
$regressed = classifiedTrendReport('fast-feedback', 'regressed', [200.0, 180.0, 160.0, 140.0, 120.0], 225.0);
|
||||
$unstable = classifiedTrendReport('fast-feedback', 'unstable', [170.0, 195.0, 168.0, 193.0, 166.0], 194.0);
|
||||
|
||||
expect($healthy['trendCurrentAssessment']['healthClass'])->toBe('healthy')
|
||||
->and($budgetNear['trendCurrentAssessment']['healthClass'])->toBe('budget-near')
|
||||
->and($stableBudgetBreach['trendCurrentAssessment']['healthClass'])->toBe('regressed')
|
||||
->and($stableBudgetBreach['trendCurrentAssessment']['summaryLine'])->toContain('over budget')
|
||||
->and($trendingWorse['trendCurrentAssessment']['healthClass'])->toBe('trending-worse')
|
||||
->and($regressed['trendCurrentAssessment']['healthClass'])->toBe('regressed')
|
||||
->and($unstable['trendCurrentAssessment']['healthClass'])->toBe('unstable')
|
||||
|
||||
@ -16,8 +16,7 @@ public function __construct(
|
||||
public readonly ?int $baselineDeltaTargetPercent = null,
|
||||
public readonly ?string $notes = null,
|
||||
public readonly ?string $reviewCadence = null,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $budget
|
||||
@ -273,9 +272,69 @@ public static function evaluateTriggerAwareBudgetProfile(array $profile, float $
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['laneId', 'triggerClass', 'enforcementMode', 'lifecycleState'] as $requiredStringKey) {
|
||||
if (! is_string($profile[$requiredStringKey]) || trim($profile[$requiredStringKey]) === '') {
|
||||
throw new InvalidArgumentException(sprintf('Trigger-aware budget profile [%s] must be a non-empty string.', $requiredStringKey));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['baseThresholdSeconds', 'varianceAllowanceSeconds', 'effectiveThresholdSeconds'] as $requiredNumericKey) {
|
||||
if (! is_int($profile[$requiredNumericKey]) && ! is_float($profile[$requiredNumericKey])) {
|
||||
throw new InvalidArgumentException(sprintf('Trigger-aware budget profile [%s] must be numeric.', $requiredNumericKey));
|
||||
}
|
||||
|
||||
if (! is_finite((float) $profile[$requiredNumericKey])) {
|
||||
throw new InvalidArgumentException(sprintf('Trigger-aware budget profile [%s] must be finite.', $requiredNumericKey));
|
||||
}
|
||||
}
|
||||
|
||||
$baseThresholdSeconds = (float) $profile['baseThresholdSeconds'];
|
||||
$varianceAllowanceSeconds = (float) $profile['varianceAllowanceSeconds'];
|
||||
$effectiveThresholdSeconds = (float) $profile['effectiveThresholdSeconds'];
|
||||
|
||||
if ($baseThresholdSeconds <= 0 || $varianceAllowanceSeconds < 0 || $effectiveThresholdSeconds <= 0) {
|
||||
throw new InvalidArgumentException('Trigger-aware budget thresholds must define a positive base/effective threshold and a non-negative variance allowance.');
|
||||
}
|
||||
|
||||
if (abs(($baseThresholdSeconds + $varianceAllowanceSeconds) - $effectiveThresholdSeconds) > 0.000001) {
|
||||
throw new InvalidArgumentException('Trigger-aware budget effectiveThresholdSeconds must equal baseThresholdSeconds plus varianceAllowanceSeconds.');
|
||||
}
|
||||
|
||||
if (! in_array($profile['enforcementMode'], ['hard-fail', 'soft-warn', 'trend-only'], true)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown enforcement mode [%s].', $profile['enforcementMode']));
|
||||
}
|
||||
|
||||
$authoritativeProfile = self::enforcementProfile(
|
||||
(string) $profile['laneId'],
|
||||
(string) $profile['triggerClass'],
|
||||
);
|
||||
|
||||
foreach (['baseThresholdSeconds', 'varianceAllowanceSeconds', 'effectiveThresholdSeconds'] as $authoritativeNumericKey) {
|
||||
if (abs((float) $profile[$authoritativeNumericKey] - (float) $authoritativeProfile[$authoritativeNumericKey]) > 0.000001) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Trigger-aware budget profile [%s] must match the authoritative [%s/%s] enforcement profile.',
|
||||
$authoritativeNumericKey,
|
||||
$profile['laneId'],
|
||||
$profile['triggerClass'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['enforcementMode', 'lifecycleState'] as $authoritativeStringKey) {
|
||||
if ($profile[$authoritativeStringKey] !== $authoritativeProfile[$authoritativeStringKey]) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Trigger-aware budget profile [%s] must match the authoritative [%s/%s] enforcement profile.',
|
||||
$authoritativeStringKey,
|
||||
$profile['laneId'],
|
||||
$profile['triggerClass'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_finite($measuredSeconds) || $measuredSeconds < 0) {
|
||||
throw new InvalidArgumentException('Trigger-aware budget measuredSeconds must be finite and non-negative.');
|
||||
}
|
||||
|
||||
$budgetStatus = 'within-budget';
|
||||
|
||||
if ($measuredSeconds > $effectiveThresholdSeconds) {
|
||||
@ -620,5 +679,4 @@ public static function automaticRecalibrationDecisions(
|
||||
|
||||
return $decisionRecords;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Throwable;
|
||||
|
||||
final class TestLaneManifest
|
||||
{
|
||||
@ -2459,8 +2460,21 @@ public static function currentCiContext(string $laneId): array
|
||||
|
||||
if (is_string($workflowId) && $workflowId !== '') {
|
||||
try {
|
||||
$workflowProfile = self::workflowProfile($workflowId);
|
||||
$executionValidation = self::validateWorkflowExecution($workflowId, $laneId);
|
||||
$triggerClass ??= self::workflowProfile($workflowId)['triggerClass'];
|
||||
$expectedTriggerClass = (string) $workflowProfile['triggerClass'];
|
||||
|
||||
if ($triggerClass === null) {
|
||||
$triggerClass = $expectedTriggerClass;
|
||||
} elseif ($triggerClass !== $expectedTriggerClass) {
|
||||
$executionValidation['entryPointResolved'] = false;
|
||||
$executionValidation['valid'] = false;
|
||||
$executionValidation['primaryFailureClassId'] = 'wrapper-failure';
|
||||
$executionValidation['unresolvedEntryPoints'] = array_values(array_unique([
|
||||
...($executionValidation['unresolvedEntryPoints'] ?? []),
|
||||
'trigger-class',
|
||||
]));
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
$executionValidation = [
|
||||
'entryPointResolved' => false,
|
||||
@ -2584,48 +2598,149 @@ public static function runLane(string $laneId): int
|
||||
$capturedOutput .= $buffer;
|
||||
});
|
||||
|
||||
TestLaneReport::finalizeLane(
|
||||
$executionExitCode = $process->getExitCode() ?? 1;
|
||||
|
||||
$report = TestLaneReport::finalizeLane(
|
||||
$laneId,
|
||||
microtime(true) - $startedAt,
|
||||
$capturedOutput,
|
||||
exitCode: $process->getExitCode() ?? 1,
|
||||
exitCode: $executionExitCode,
|
||||
);
|
||||
|
||||
return $process->getExitCode() ?? 1;
|
||||
return (int) ($report['finalResult']['exitCode'] ?? $executionExitCode);
|
||||
}
|
||||
|
||||
public static function renderLatestReport(string $laneId, ?string $comparisonProfile = null): int
|
||||
{
|
||||
$artifactPaths = TestLaneReport::artifactPaths($laneId);
|
||||
public static function renderLatestReport(
|
||||
string $laneId,
|
||||
?string $comparisonProfile = null,
|
||||
?string $artifactDirectory = null,
|
||||
): int {
|
||||
$artifactPaths = TestLaneReport::artifactPaths($laneId, $artifactDirectory);
|
||||
$reportPath = self::absolutePath($artifactPaths['report']);
|
||||
$wallClockSeconds = 0.0;
|
||||
$existingReport = null;
|
||||
$reportInput = [
|
||||
'path' => $reportPath,
|
||||
'exists' => false,
|
||||
'parseValid' => false,
|
||||
'status' => 'missing',
|
||||
];
|
||||
|
||||
if (is_file($reportPath)) {
|
||||
$existingReport = json_decode((string) file_get_contents($reportPath), true);
|
||||
$wallClockSeconds = (float) ($existingReport['wallClockSeconds'] ?? 0.0);
|
||||
$reportInput['exists'] = true;
|
||||
|
||||
try {
|
||||
$decodedReport = json_decode(
|
||||
(string) file_get_contents($reportPath),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
if (is_array($decodedReport)
|
||||
&& ! array_is_list($decodedReport)
|
||||
&& is_string($decodedReport['laneId'] ?? null)
|
||||
&& $decodedReport['laneId'] === $laneId) {
|
||||
$existingReport = $decodedReport;
|
||||
$wallClockSeconds = (float) ($existingReport['wallClockSeconds'] ?? 0.0);
|
||||
$reportInput['parseValid'] = true;
|
||||
$reportInput['status'] = 'valid';
|
||||
} else {
|
||||
$reportInput['status'] = 'malformed';
|
||||
$reportInput['reason'] = 'invalid-report-shape';
|
||||
}
|
||||
} catch (Throwable $throwable) {
|
||||
$reportInput['status'] = 'malformed';
|
||||
$reportInput['errorClass'] = $throwable::class;
|
||||
}
|
||||
}
|
||||
|
||||
$parsed = TestLaneReport::parseJUnit(self::absolutePath($artifactPaths['junit']), $laneId);
|
||||
$profileOutputPath = self::absolutePath($artifactPaths['profile']);
|
||||
$ciContext = self::currentCiContext($laneId);
|
||||
|
||||
if ($ciContext === [] && is_array($existingReport['ciContext'] ?? null)) {
|
||||
$ciContext = $existingReport['ciContext'];
|
||||
}
|
||||
|
||||
if ((! is_string($ciContext['triggerClass'] ?? null) || $ciContext['triggerClass'] === '')
|
||||
&& is_array($existingReport['ciBudgetEvaluation'] ?? null)) {
|
||||
$storedTriggerClass = $existingReport['ciBudgetEvaluation']['triggerClass'] ?? null;
|
||||
|
||||
if (is_string($storedTriggerClass) && $storedTriggerClass !== '') {
|
||||
$ciContext['triggerClass'] = $storedTriggerClass;
|
||||
}
|
||||
}
|
||||
|
||||
TestLaneReport::invalidatePublishedResultArtifacts($laneId, $artifactDirectory);
|
||||
|
||||
$report = TestLaneReport::buildReport(
|
||||
laneId: $laneId,
|
||||
wallClockSeconds: $wallClockSeconds,
|
||||
slowestEntries: $parsed['slowestEntries'],
|
||||
durationsByFile: $parsed['durationsByFile'],
|
||||
artifactDirectory: $artifactDirectory,
|
||||
comparisonProfile: $comparisonProfile,
|
||||
ciContext: $ciContext,
|
||||
);
|
||||
$report['execution'] = is_array($existingReport['execution'] ?? null)
|
||||
? $existingReport['execution']
|
||||
: TestLaneReport::executionResult(null);
|
||||
$report['evidence'] = $parsed['evidence'];
|
||||
$report['reportInput'] = $reportInput;
|
||||
$report['reportGeneration'] = ['status' => 'succeeded'];
|
||||
$report['priorClaimedStatus'] = $existingReport['finalResult']['status']
|
||||
?? $existingReport['ciSummary']['finalStatus']
|
||||
?? $existingReport['ciSummary']['testStatus']
|
||||
?? null;
|
||||
|
||||
TestLaneReport::writeArtifacts(
|
||||
laneId: $laneId,
|
||||
report: $report,
|
||||
profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null,
|
||||
exitCode: 0,
|
||||
);
|
||||
try {
|
||||
TestLaneReport::writeArtifacts(
|
||||
laneId: $laneId,
|
||||
report: $report,
|
||||
profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
exitCode: is_int($report['execution']['exitCode'] ?? null) ? $report['execution']['exitCode'] : null,
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
$report['reportGeneration'] = [
|
||||
'status' => 'failed',
|
||||
'errorClass' => $throwable::class,
|
||||
'message' => $throwable->getMessage(),
|
||||
];
|
||||
$report['artifactPublication'] = TestLaneReport::artifactPublicationStatus($laneId, $artifactDirectory);
|
||||
$report['finalResult'] = TestLaneReport::deriveFinalResult(
|
||||
execution: $report['execution'],
|
||||
evidence: $report['evidence'],
|
||||
reportGeneration: $report['reportGeneration'],
|
||||
reportInput: $report['reportInput'],
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
claimedStatus: is_string($report['priorClaimedStatus']) ? $report['priorClaimedStatus'] : null,
|
||||
);
|
||||
unset($report['priorClaimedStatus']);
|
||||
$report['ciSummary'] = TestLaneReport::buildCiSummary(
|
||||
report: $report,
|
||||
exitCode: is_int($report['execution']['exitCode'] ?? null) ? $report['execution']['exitCode'] : null,
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
);
|
||||
|
||||
echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
|
||||
return 0;
|
||||
return (int) $report['finalResult']['exitCode'];
|
||||
}
|
||||
|
||||
$storedReport = json_decode((string) file_get_contents($reportPath), true, 512, JSON_THROW_ON_ERROR);
|
||||
$storedReport = is_array($storedReport) ? $storedReport : $report;
|
||||
|
||||
echo json_encode($storedReport, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
|
||||
return (int) ($storedReport['finalResult']['exitCode'] ?? 1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use RuntimeException;
|
||||
use SimpleXMLElement;
|
||||
use Throwable;
|
||||
use ZipArchive;
|
||||
|
||||
final class TestLaneReport
|
||||
@ -106,6 +108,183 @@ public static function classifyPrimaryFailure(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{exitCode: int|null, status: string, capturedAt: string}
|
||||
*/
|
||||
public static function executionResult(?int $exitCode): array
|
||||
{
|
||||
return [
|
||||
'exitCode' => $exitCode,
|
||||
'status' => match (true) {
|
||||
$exitCode === null => 'unknown',
|
||||
$exitCode === 0 => 'passed',
|
||||
default => 'failed',
|
||||
},
|
||||
'capturedAt' => gmdate('c'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $execution
|
||||
* @param array<string, mixed> $evidence
|
||||
* @param array<string, mixed> $reportGeneration
|
||||
* @param array<string, mixed>|null $reportInput
|
||||
* @param array<string, mixed>|null $artifactPublicationStatus
|
||||
* @param array<string, mixed>|null $budgetOutcome
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function deriveFinalResult(
|
||||
array $execution,
|
||||
array $evidence,
|
||||
array $reportGeneration,
|
||||
?array $reportInput = null,
|
||||
?array $artifactPublicationStatus = null,
|
||||
?array $budgetOutcome = null,
|
||||
bool $entryPointResolved = true,
|
||||
bool $workflowLaneMatched = true,
|
||||
?string $claimedStatus = null,
|
||||
): array {
|
||||
$failureContexts = [];
|
||||
$advisoryContexts = [];
|
||||
$appendFailure = static function (string $contextId, array $details = []) use (&$failureContexts): void {
|
||||
if (in_array($contextId, array_column($failureContexts, 'contextId'), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failureContexts[] = array_merge(['contextId' => $contextId], $details);
|
||||
};
|
||||
$appendAdvisory = static function (string $contextId, array $details = []) use (&$advisoryContexts): void {
|
||||
if (in_array($contextId, array_column($advisoryContexts, 'contextId'), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$advisoryContexts[] = array_merge(['contextId' => $contextId], $details);
|
||||
};
|
||||
|
||||
$executionExitCode = is_int($execution['exitCode'] ?? null)
|
||||
? (int) $execution['exitCode']
|
||||
: null;
|
||||
$executionStatus = (string) ($execution['status'] ?? 'unknown');
|
||||
$evidenceStatus = (string) ($evidence['status'] ?? 'missing');
|
||||
|
||||
if ($executionStatus === 'unknown' || $executionExitCode === null) {
|
||||
$appendFailure('execution-missing');
|
||||
} elseif ($executionExitCode !== 0 || $executionStatus === 'failed') {
|
||||
$appendFailure('test-failure', ['exitCode' => $executionExitCode]);
|
||||
}
|
||||
|
||||
switch ($evidenceStatus) {
|
||||
case 'missing':
|
||||
$appendFailure('evidence-missing', ['path' => $evidence['path'] ?? null]);
|
||||
break;
|
||||
case 'malformed':
|
||||
$appendFailure('evidence-malformed', ['path' => $evidence['path'] ?? null]);
|
||||
break;
|
||||
case 'empty':
|
||||
$appendFailure('evidence-empty', ['path' => $evidence['path'] ?? null]);
|
||||
break;
|
||||
case 'failed':
|
||||
$appendFailure('junit-failures', ['failureCount' => (int) ($evidence['failureCount'] ?? 0)]);
|
||||
break;
|
||||
case 'error-containing':
|
||||
$appendFailure('junit-errors', ['errorCount' => (int) ($evidence['errorCount'] ?? 0)]);
|
||||
break;
|
||||
}
|
||||
|
||||
$evidenceClaimsSuccess = in_array($evidenceStatus, ['passed', 'skipped'], true);
|
||||
$evidenceClaimsFailure = in_array($evidenceStatus, ['missing', 'malformed', 'empty', 'failed', 'error-containing'], true);
|
||||
|
||||
if (($executionExitCode === 0 && $evidenceClaimsFailure)
|
||||
|| ($executionExitCode !== null && $executionExitCode !== 0 && $evidenceClaimsSuccess)
|
||||
|| ($claimedStatus === 'passed' && $failureContexts !== [])) {
|
||||
$appendFailure('evidence-contradiction', [
|
||||
'executionStatus' => $executionStatus,
|
||||
'evidenceStatus' => $evidenceStatus,
|
||||
'claimedStatus' => $claimedStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
if (($reportGeneration['status'] ?? 'succeeded') !== 'succeeded') {
|
||||
$appendFailure('report-generation-failure', [
|
||||
'errorClass' => $reportGeneration['errorClass'] ?? null,
|
||||
'message' => $reportGeneration['message'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
if (is_array($reportInput)) {
|
||||
if (($reportInput['status'] ?? null) === 'missing') {
|
||||
$appendFailure('report-input-missing', ['path' => $reportInput['path'] ?? null]);
|
||||
} elseif (($reportInput['status'] ?? null) === 'malformed') {
|
||||
$appendFailure('report-input-malformed', ['path' => $reportInput['path'] ?? null]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($artifactPublicationStatus) && ($artifactPublicationStatus['complete'] ?? false) !== true) {
|
||||
$appendFailure('artifact-publication-failure', [
|
||||
'missingRequiredArtifacts' => $artifactPublicationStatus['missingRequiredArtifacts'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $entryPointResolved || ! $workflowLaneMatched) {
|
||||
$appendFailure('wrapper-failure', [
|
||||
'entryPointResolved' => $entryPointResolved,
|
||||
'workflowLaneMatched' => $workflowLaneMatched,
|
||||
]);
|
||||
}
|
||||
|
||||
if (is_array($budgetOutcome)) {
|
||||
$budgetStatus = $budgetOutcome['budgetStatus'] ?? null;
|
||||
$budgetBlockingStatus = $budgetOutcome['blockingStatus'] ?? null;
|
||||
$budgetContext = [
|
||||
'policyId' => $budgetOutcome['policyId'] ?? null,
|
||||
'laneId' => $budgetOutcome['laneId'] ?? null,
|
||||
'triggerClass' => $budgetOutcome['triggerClass'] ?? null,
|
||||
'budgetStatus' => $budgetStatus,
|
||||
'blockingStatus' => $budgetBlockingStatus,
|
||||
'measuredSeconds' => $budgetOutcome['measuredSeconds'] ?? null,
|
||||
'effectiveThresholdSeconds' => $budgetOutcome['effectiveThresholdSeconds'] ?? null,
|
||||
];
|
||||
|
||||
if (! is_string($budgetBlockingStatus)
|
||||
|| ! in_array($budgetBlockingStatus, ['informational', 'non-blocking-warning', 'blocking'], true)) {
|
||||
$appendFailure('budget-evidence-malformed', $budgetContext);
|
||||
} elseif ($budgetBlockingStatus === 'blocking') {
|
||||
$appendFailure('blocking-budget-breach', $budgetContext);
|
||||
} elseif ($budgetBlockingStatus === 'non-blocking-warning') {
|
||||
$appendAdvisory('budget-advisory-warning', $budgetContext);
|
||||
}
|
||||
}
|
||||
|
||||
if ($claimedStatus === 'failed'
|
||||
&& $executionExitCode === 0
|
||||
&& $evidenceClaimsSuccess
|
||||
&& $failureContexts === []) {
|
||||
$appendFailure('report-claim-contradiction', [
|
||||
'executionStatus' => $executionStatus,
|
||||
'evidenceStatus' => $evidenceStatus,
|
||||
'claimedStatus' => $claimedStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
$finalExitCode = $executionExitCode !== null && $executionExitCode !== 0
|
||||
? $executionExitCode
|
||||
: ($failureContexts === [] ? 0 : 1);
|
||||
|
||||
return [
|
||||
'status' => $finalExitCode === 0 ? 'passed' : 'failed',
|
||||
'exitCode' => $finalExitCode,
|
||||
'blockingStatus' => match (true) {
|
||||
$finalExitCode !== 0 => 'blocking',
|
||||
$advisoryContexts !== [] => 'non-blocking-warning',
|
||||
default => 'informational',
|
||||
},
|
||||
'failureContextIds' => array_column($failureContexts, 'contextId'),
|
||||
'failureContexts' => $failureContexts,
|
||||
'advisoryContextIds' => array_column($advisoryContexts, 'contextId'),
|
||||
'advisoryContexts' => $advisoryContexts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $report
|
||||
* @param array<string, mixed>|null $budgetOutcome
|
||||
@ -114,7 +293,7 @@ public static function classifyPrimaryFailure(
|
||||
*/
|
||||
public static function buildCiSummary(
|
||||
array $report,
|
||||
?int $exitCode = 0,
|
||||
?int $exitCode = null,
|
||||
?array $budgetOutcome = null,
|
||||
?array $artifactPublicationStatus = null,
|
||||
bool $entryPointResolved = true,
|
||||
@ -140,16 +319,37 @@ public static function buildCiSummary(
|
||||
$scopeBoundaryNote = (string) ($report['scopeBoundaryNote']
|
||||
?? (TestLaneManifest::lane((string) $report['laneId'])['scopeBoundaryNote'] ?? ''));
|
||||
|
||||
$execution = is_array($report['execution'] ?? null)
|
||||
? $report['execution']
|
||||
: self::executionResult($exitCode);
|
||||
$finalResult = is_array($report['finalResult'] ?? null)
|
||||
? $report['finalResult']
|
||||
: [
|
||||
'status' => $exitCode === 0 ? 'passed' : 'failed',
|
||||
'exitCode' => $exitCode === 0 ? 0 : 1,
|
||||
'failureContexts' => [],
|
||||
];
|
||||
|
||||
if (($finalResult['status'] ?? 'failed') === 'failed' && $blockingStatus === 'informational') {
|
||||
$blockingStatus = 'blocking';
|
||||
}
|
||||
|
||||
return [
|
||||
'runId' => (string) (getenv('GITEA_RUN_ID') ?: getenv('GITHUB_RUN_ID') ?: sprintf('local-%s', $report['laneId'])),
|
||||
'workflowId' => (string) ($report['ciContext']['workflowId'] ?? sprintf('local-%s', $report['laneId'])),
|
||||
'laneId' => (string) $report['laneId'],
|
||||
'testStatus' => $exitCode === 0 ? 'passed' : 'failed',
|
||||
'testStatus' => (string) ($execution['status'] ?? 'unknown'),
|
||||
'executionExitCode' => $execution['exitCode'] ?? null,
|
||||
'evidenceStatus' => (string) ($report['evidence']['status'] ?? 'missing'),
|
||||
'artifactStatus' => ($artifactPublicationStatus['complete'] ?? false) ? 'complete' : 'incomplete',
|
||||
'budgetStatus' => $budgetStatus,
|
||||
'blockingStatus' => $blockingStatus,
|
||||
'primaryFailureClassId' => $primaryFailureClassId,
|
||||
'publishedArtifacts' => $artifactPublicationStatus['publishedArtifacts'],
|
||||
'finalStatus' => (string) ($finalResult['status'] ?? 'failed'),
|
||||
'finalExitCode' => (int) ($finalResult['exitCode'] ?? 1),
|
||||
'failureContexts' => $finalResult['failureContexts'] ?? [],
|
||||
'advisoryContexts' => $finalResult['advisoryContexts'] ?? [],
|
||||
'publishedArtifacts' => $artifactPublicationStatus['publishedArtifacts'] ?? [],
|
||||
'scopeBoundaryNote' => $scopeBoundaryNote !== '' ? $scopeBoundaryNote : null,
|
||||
];
|
||||
}
|
||||
@ -168,6 +368,7 @@ public static function stageArtifacts(string $laneId, string $stagingDirectory,
|
||||
|
||||
$stagedArtifacts = [];
|
||||
$missingRequiredArtifacts = [];
|
||||
$failedArtifacts = [];
|
||||
|
||||
foreach (array_merge($artifactContract['requiredFiles'], $artifactContract['optionalFiles']) as $artifactFile) {
|
||||
if (! array_key_exists($artifactFile, $artifactFileMap)) {
|
||||
@ -189,7 +390,20 @@ public static function stageArtifacts(string $laneId, string $stagingDirectory,
|
||||
$stagedFileName = self::stagedArtifactName((string) $artifactContract['stagedNamePattern'], $laneId, $artifactFile);
|
||||
$targetAbsolutePath = $stagingRoot.DIRECTORY_SEPARATOR.$stagedFileName;
|
||||
|
||||
copy($sourceAbsolutePath, $targetAbsolutePath);
|
||||
if (! self::copyFile($sourceAbsolutePath, $targetAbsolutePath)) {
|
||||
$failedArtifacts[] = [
|
||||
'artifactType' => $artifactFile,
|
||||
'sourcePath' => $sourceRelativePath,
|
||||
'targetPath' => $targetAbsolutePath,
|
||||
'required' => $required,
|
||||
];
|
||||
|
||||
if ($required) {
|
||||
$missingRequiredArtifacts[] = $artifactFile;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$stagedArtifacts[] = [
|
||||
'artifactType' => $artifactFile,
|
||||
@ -206,6 +420,7 @@ public static function stageArtifacts(string $laneId, string $stagingDirectory,
|
||||
'complete' => $missingRequiredArtifacts === [],
|
||||
'primaryFailureClassId' => $missingRequiredArtifacts === [] ? null : 'artifact-publication-failure',
|
||||
'missingRequiredArtifacts' => $missingRequiredArtifacts,
|
||||
'failedArtifacts' => $failedArtifacts,
|
||||
'uploadGroupName' => $artifactContract['uploadGroupName'],
|
||||
];
|
||||
}
|
||||
@ -232,24 +447,22 @@ public static function hydrateTrendHistory(
|
||||
: null;
|
||||
|
||||
if (is_string($resolvedHistoryFile) && is_file($resolvedHistoryFile)) {
|
||||
copy($resolvedHistoryFile, $targetPath);
|
||||
|
||||
return [
|
||||
'laneId' => $laneId,
|
||||
'targetPath' => $targetPath,
|
||||
'hydrated' => true,
|
||||
'sourceType' => 'history-file',
|
||||
'sourcePath' => $resolvedHistoryFile,
|
||||
];
|
||||
if (self::copyFile($resolvedHistoryFile, $targetPath)) {
|
||||
return [
|
||||
'laneId' => $laneId,
|
||||
'targetPath' => $targetPath,
|
||||
'hydrated' => true,
|
||||
'sourceType' => 'history-file',
|
||||
'sourcePath' => $resolvedHistoryFile,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($resolvedBundlePath) && $resolvedBundlePath !== '') {
|
||||
if (is_dir($resolvedBundlePath)) {
|
||||
$bundleHistoryPath = self::findTrendHistoryInDirectory($laneId, $resolvedBundlePath);
|
||||
|
||||
if (is_string($bundleHistoryPath)) {
|
||||
copy($bundleHistoryPath, $targetPath);
|
||||
|
||||
if (is_string($bundleHistoryPath) && self::copyFile($bundleHistoryPath, $targetPath)) {
|
||||
return [
|
||||
'laneId' => $laneId,
|
||||
'targetPath' => $targetPath,
|
||||
@ -259,7 +472,7 @@ public static function hydrateTrendHistory(
|
||||
];
|
||||
}
|
||||
} elseif (is_file($resolvedBundlePath) && str_ends_with(strtolower($resolvedBundlePath), '.zip')) {
|
||||
$zip = new ZipArchive();
|
||||
$zip = new ZipArchive;
|
||||
|
||||
if ($zip->open($resolvedBundlePath) === true) {
|
||||
$entryName = self::findTrendHistoryInZip($laneId, $zip);
|
||||
@ -268,9 +481,7 @@ public static function hydrateTrendHistory(
|
||||
$contents = $zip->getFromName($entryName);
|
||||
$zip->close();
|
||||
|
||||
if (is_string($contents) && $contents !== '') {
|
||||
file_put_contents($targetPath, $contents);
|
||||
|
||||
if (is_string($contents) && $contents !== '' && self::writeFile($targetPath, $contents)) {
|
||||
return [
|
||||
'laneId' => $laneId,
|
||||
'targetPath' => $targetPath,
|
||||
@ -297,7 +508,7 @@ public static function hydrateTrendHistory(
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{slowestEntries: list<array<string, mixed>>, durationsByFile: array<string, float>}
|
||||
* @return array{slowestEntries: list<array<string, mixed>>, durationsByFile: array<string, float>, evidence: array<string, mixed>}
|
||||
*/
|
||||
public static function parseJUnit(string $filePath, string $laneId): array
|
||||
{
|
||||
@ -305,6 +516,16 @@ public static function parseJUnit(string $filePath, string $laneId): array
|
||||
return [
|
||||
'slowestEntries' => [],
|
||||
'durationsByFile' => [],
|
||||
'evidence' => [
|
||||
'path' => $filePath,
|
||||
'exists' => false,
|
||||
'parseValid' => false,
|
||||
'testCount' => 0,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
'status' => 'missing',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -317,13 +538,28 @@ public static function parseJUnit(string $filePath, string $laneId): array
|
||||
return [
|
||||
'slowestEntries' => [],
|
||||
'durationsByFile' => [],
|
||||
'evidence' => [
|
||||
'path' => $filePath,
|
||||
'exists' => true,
|
||||
'parseValid' => false,
|
||||
'testCount' => 0,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
'status' => 'malformed',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$slowestEntries = [];
|
||||
$durationsByFile = [];
|
||||
$testcases = $xml->xpath('//testcase') ?: [];
|
||||
$testCount = count($testcases);
|
||||
$failureCount = count($xml->xpath('//testcase/failure') ?: []);
|
||||
$errorCount = count($xml->xpath('//testcase/error') ?: []);
|
||||
$skippedCount = count($xml->xpath('//testcase/skipped') ?: []);
|
||||
|
||||
foreach ($xml->xpath('//testcase') ?: [] as $testcase) {
|
||||
foreach ($testcases as $testcase) {
|
||||
$rawSubject = trim((string) ($testcase['file'] ?? ''));
|
||||
$subject = $rawSubject !== '' ? $rawSubject : trim((string) ($testcase['name'] ?? 'unknown-testcase'));
|
||||
$duration = round((float) ($testcase['time'] ?? 0.0), 6);
|
||||
@ -346,6 +582,22 @@ public static function parseJUnit(string $filePath, string $laneId): array
|
||||
return [
|
||||
'slowestEntries' => array_slice($slowestEntries, 0, 10),
|
||||
'durationsByFile' => $durationsByFile,
|
||||
'evidence' => [
|
||||
'path' => $filePath,
|
||||
'exists' => true,
|
||||
'parseValid' => true,
|
||||
'testCount' => $testCount,
|
||||
'failureCount' => $failureCount,
|
||||
'errorCount' => $errorCount,
|
||||
'skippedCount' => $skippedCount,
|
||||
'status' => match (true) {
|
||||
$errorCount > 0 => 'error-containing',
|
||||
$failureCount > 0 => 'failed',
|
||||
$testCount === 0 => 'empty',
|
||||
$skippedCount === $testCount => 'skipped',
|
||||
default => 'passed',
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -378,7 +630,10 @@ public static function buildReport(
|
||||
$resolvedCiContext = array_filter($ciContext ?? TestLaneManifest::currentCiContext($laneId), static fn (mixed $value): bool => $value !== null);
|
||||
$ciBudgetEvaluation = null;
|
||||
|
||||
if (is_string($resolvedCiContext['triggerClass'] ?? null) && $resolvedCiContext['triggerClass'] !== '') {
|
||||
if (is_string($resolvedCiContext['triggerClass'] ?? null)
|
||||
&& $resolvedCiContext['triggerClass'] !== ''
|
||||
&& (bool) ($resolvedCiContext['entryPointResolved'] ?? true)
|
||||
&& (bool) ($resolvedCiContext['workflowLaneMatched'] ?? true)) {
|
||||
$ciBudgetEvaluation = TestLaneBudget::evaluateLaneForTrigger(
|
||||
$laneId,
|
||||
(string) $resolvedCiContext['triggerClass'],
|
||||
@ -537,60 +792,122 @@ public static function writeArtifacts(
|
||||
array $report,
|
||||
?string $profileOutput = null,
|
||||
?string $artifactDirectory = null,
|
||||
?int $exitCode = 0,
|
||||
?int $exitCode = null,
|
||||
): array {
|
||||
$artifactPaths = self::artifactPaths($laneId, $artifactDirectory);
|
||||
$parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId);
|
||||
$report['execution'] = is_array($report['execution'] ?? null)
|
||||
? $report['execution']
|
||||
: self::executionResult($exitCode);
|
||||
$report['evidence'] = is_array($report['evidence'] ?? null)
|
||||
? $report['evidence']
|
||||
: $parsed['evidence'];
|
||||
$report['reportGeneration'] = is_array($report['reportGeneration'] ?? null)
|
||||
? $report['reportGeneration']
|
||||
: ['status' => 'succeeded'];
|
||||
$finalReportGeneration = $report['reportGeneration'];
|
||||
$report['finalResult'] = self::deriveFinalResult(
|
||||
execution: $report['execution'],
|
||||
evidence: $report['evidence'],
|
||||
reportGeneration: $report['reportGeneration'],
|
||||
reportInput: is_array($report['reportInput'] ?? null) ? $report['reportInput'] : null,
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
claimedStatus: is_string($report['priorClaimedStatus'] ?? null) ? $report['priorClaimedStatus'] : null,
|
||||
);
|
||||
|
||||
self::ensureDirectory(dirname(TestLaneManifest::absolutePath($artifactPaths['summary'])));
|
||||
self::invalidatePublishedResultArtifacts($laneId, $artifactDirectory);
|
||||
|
||||
file_put_contents(
|
||||
$provisionalReport = $report;
|
||||
$provisionalReport['reportGeneration'] = [
|
||||
'status' => 'failed',
|
||||
'message' => 'Artifact publication has not completed.',
|
||||
];
|
||||
$provisionalReport['artifactPublication'] = self::artifactPublicationStatus($laneId, $artifactDirectory);
|
||||
$provisionalReport['finalResult'] = self::deriveFinalResult(
|
||||
execution: $provisionalReport['execution'],
|
||||
evidence: $provisionalReport['evidence'],
|
||||
reportGeneration: $provisionalReport['reportGeneration'],
|
||||
reportInput: is_array($provisionalReport['reportInput'] ?? null) ? $provisionalReport['reportInput'] : null,
|
||||
artifactPublicationStatus: $provisionalReport['artifactPublication'],
|
||||
budgetOutcome: is_array($provisionalReport['ciBudgetEvaluation'] ?? null) ? $provisionalReport['ciBudgetEvaluation'] : null,
|
||||
entryPointResolved: (bool) ($provisionalReport['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($provisionalReport['ciContext']['workflowLaneMatched'] ?? true),
|
||||
claimedStatus: is_string($provisionalReport['priorClaimedStatus'] ?? null) ? $provisionalReport['priorClaimedStatus'] : null,
|
||||
);
|
||||
unset($provisionalReport['priorClaimedStatus']);
|
||||
$provisionalReport['ciSummary'] = self::buildCiSummary(
|
||||
report: $provisionalReport,
|
||||
exitCode: is_int($provisionalReport['execution']['exitCode'] ?? null) ? $provisionalReport['execution']['exitCode'] : null,
|
||||
budgetOutcome: is_array($provisionalReport['ciBudgetEvaluation'] ?? null) ? $provisionalReport['ciBudgetEvaluation'] : null,
|
||||
artifactPublicationStatus: $provisionalReport['artifactPublication'],
|
||||
entryPointResolved: (bool) ($provisionalReport['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($provisionalReport['ciContext']['workflowLaneMatched'] ?? true),
|
||||
);
|
||||
|
||||
self::writeRequiredFile(
|
||||
TestLaneManifest::absolutePath($artifactPaths['summary']),
|
||||
self::buildSummaryMarkdown($report),
|
||||
self::buildSummaryMarkdown($provisionalReport),
|
||||
);
|
||||
|
||||
if (is_string($profileOutput) && trim($profileOutput) !== '') {
|
||||
file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput);
|
||||
self::writeRequiredFile(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput);
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
self::writeRequiredFile(
|
||||
TestLaneManifest::absolutePath($artifactPaths['budget']),
|
||||
json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
json_encode(self::budgetPayload($provisionalReport), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
self::writeRequiredFile(
|
||||
TestLaneManifest::absolutePath($artifactPaths['report']),
|
||||
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
json_encode($provisionalReport, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
self::writeRequiredFile(
|
||||
TestLaneManifest::absolutePath($artifactPaths['trendHistory']),
|
||||
json_encode($report['trendHistoryArtifact'] ?? [], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
|
||||
$report['reportGeneration'] = $finalReportGeneration;
|
||||
$report['artifactPublication'] = self::artifactPublicationStatus($laneId, $artifactDirectory);
|
||||
$report['finalResult'] = self::deriveFinalResult(
|
||||
execution: $report['execution'],
|
||||
evidence: $report['evidence'],
|
||||
reportGeneration: $report['reportGeneration'],
|
||||
reportInput: is_array($report['reportInput'] ?? null) ? $report['reportInput'] : null,
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
claimedStatus: is_string($report['priorClaimedStatus'] ?? null) ? $report['priorClaimedStatus'] : null,
|
||||
);
|
||||
unset($report['priorClaimedStatus']);
|
||||
$report['ciSummary'] = self::buildCiSummary(
|
||||
report: $report,
|
||||
exitCode: $exitCode,
|
||||
exitCode: is_int($report['execution']['exitCode'] ?? null) ? $report['execution']['exitCode'] : null,
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
TestLaneManifest::absolutePath($artifactPaths['summary']),
|
||||
self::buildSummaryMarkdown($report),
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
TestLaneManifest::absolutePath($artifactPaths['budget']),
|
||||
json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
TestLaneManifest::absolutePath($artifactPaths['report']),
|
||||
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
self::commitFinalResultArtifacts([
|
||||
TestLaneManifest::absolutePath($artifactPaths['summary']) => [
|
||||
'provisional' => self::buildSummaryMarkdown($provisionalReport),
|
||||
'final' => self::buildSummaryMarkdown($report),
|
||||
],
|
||||
TestLaneManifest::absolutePath($artifactPaths['budget']) => [
|
||||
'provisional' => json_encode(self::budgetPayload($provisionalReport), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
'final' => json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
],
|
||||
TestLaneManifest::absolutePath($artifactPaths['report']) => [
|
||||
'provisional' => json_encode($provisionalReport, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
'final' => json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
],
|
||||
]);
|
||||
|
||||
return $artifactPaths;
|
||||
}
|
||||
@ -603,9 +920,11 @@ public static function finalizeLane(
|
||||
float $wallClockSeconds,
|
||||
string $capturedOutput = '',
|
||||
?string $comparisonProfile = null,
|
||||
?int $exitCode = 0,
|
||||
?int $exitCode = null,
|
||||
?string $artifactDirectory = null,
|
||||
): array {
|
||||
$artifactPaths = self::artifactPaths($laneId);
|
||||
$artifactPaths = self::artifactPaths($laneId, $artifactDirectory);
|
||||
self::invalidatePublishedResultArtifacts($laneId, $artifactDirectory);
|
||||
$parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId);
|
||||
$ciContext = TestLaneManifest::currentCiContext($laneId);
|
||||
$report = self::buildReport(
|
||||
@ -613,13 +932,58 @@ public static function finalizeLane(
|
||||
wallClockSeconds: $wallClockSeconds,
|
||||
slowestEntries: $parsed['slowestEntries'],
|
||||
durationsByFile: $parsed['durationsByFile'],
|
||||
artifactDirectory: $artifactDirectory,
|
||||
comparisonProfile: $comparisonProfile,
|
||||
ciContext: $ciContext,
|
||||
);
|
||||
$report['execution'] = self::executionResult($exitCode);
|
||||
$report['evidence'] = $parsed['evidence'];
|
||||
$report['reportGeneration'] = ['status' => 'succeeded'];
|
||||
|
||||
self::writeArtifacts($laneId, $report, $capturedOutput, exitCode: $exitCode);
|
||||
try {
|
||||
self::writeArtifacts(
|
||||
laneId: $laneId,
|
||||
report: $report,
|
||||
profileOutput: $capturedOutput,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
exitCode: $exitCode,
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
$report['reportGeneration'] = [
|
||||
'status' => 'failed',
|
||||
'errorClass' => $throwable::class,
|
||||
'message' => $throwable->getMessage(),
|
||||
];
|
||||
$report['artifactPublication'] = self::artifactPublicationStatus($laneId, $artifactDirectory);
|
||||
$report['finalResult'] = self::deriveFinalResult(
|
||||
execution: $report['execution'],
|
||||
evidence: $report['evidence'],
|
||||
reportGeneration: $report['reportGeneration'],
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
);
|
||||
$report['ciSummary'] = self::buildCiSummary(
|
||||
report: $report,
|
||||
exitCode: $exitCode,
|
||||
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
|
||||
artifactPublicationStatus: $report['artifactPublication'],
|
||||
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
|
||||
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
|
||||
);
|
||||
|
||||
return $report;
|
||||
return $report;
|
||||
}
|
||||
|
||||
$storedReport = json_decode(
|
||||
(string) file_get_contents(TestLaneManifest::absolutePath($artifactPaths['report'])),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
return is_array($storedReport) ? $storedReport : $report;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -651,7 +1015,10 @@ private static function buildSummaryMarkdown(array $report): string
|
||||
if (isset($report['ciSummary']) && is_array($report['ciSummary'])) {
|
||||
$lines[] = sprintf(
|
||||
'- CI outcome: %s / %s',
|
||||
(string) $report['ciSummary']['testStatus'],
|
||||
(string) ($report['ciSummary']['finalStatus']
|
||||
?? $report['finalResult']['status']
|
||||
?? $report['ciSummary']['testStatus']
|
||||
?? 'failed'),
|
||||
(string) $report['ciSummary']['blockingStatus'],
|
||||
);
|
||||
|
||||
@ -1119,6 +1486,11 @@ private static function budgetPayload(array $report): array
|
||||
'trendRecalibrationDecisions' => $report['trendRecalibrationDecisions'] ?? null,
|
||||
'trendWarnings' => $report['trendWarnings'] ?? null,
|
||||
'ciBudgetEvaluation' => $report['ciBudgetEvaluation'] ?? null,
|
||||
'execution' => $report['execution'] ?? null,
|
||||
'evidence' => $report['evidence'] ?? null,
|
||||
'reportInput' => $report['reportInput'] ?? null,
|
||||
'reportGeneration' => $report['reportGeneration'] ?? null,
|
||||
'finalResult' => $report['finalResult'] ?? null,
|
||||
'artifactPublication' => $report['artifactPublication'] ?? null,
|
||||
'ciSummary' => $report['ciSummary'] ?? null,
|
||||
];
|
||||
@ -1441,10 +1813,12 @@ private static function buildTrendAssessment(array $currentRecord, array $compar
|
||||
$windowStatus = 'noisy';
|
||||
$recalibrationRecommendation = 'investigate';
|
||||
$summaryLine = 'Recent samples disagree with each other, so the latest spike is treated as noise instead of a structural regression.';
|
||||
} elseif ($budgetHeadroomSeconds < 0.0 && $worseningStreak >= 2) {
|
||||
} elseif ($budgetHeadroomSeconds < 0.0) {
|
||||
$healthClass = 'regressed';
|
||||
$recalibrationRecommendation = 'review-budget';
|
||||
$summaryLine = 'Comparable runs show repeated worsening and the lane is now over budget.';
|
||||
$summaryLine = $worseningStreak >= 2
|
||||
? 'Comparable runs show repeated worsening and the lane is now over budget.'
|
||||
: 'Lane runtime is over budget, so it is classified as regressed even without a repeated worsening streak.';
|
||||
} elseif ($worseningStreak >= 2) {
|
||||
$healthClass = 'trending-worse';
|
||||
$recalibrationRecommendation = $budgetHeadroomSeconds <= $nearBudgetHeadroomSeconds ? 'review-budget' : 'investigate';
|
||||
@ -1880,6 +2254,75 @@ private static function ensureDirectory(string $directory): void
|
||||
return;
|
||||
}
|
||||
|
||||
mkdir($directory, 0777, true);
|
||||
if (! @mkdir($directory, 0777, true) && ! is_dir($directory)) {
|
||||
throw new RuntimeException(sprintf('Unable to create test-lane artifact directory [%s].', $directory));
|
||||
}
|
||||
}
|
||||
|
||||
public static function invalidatePublishedResultArtifacts(string $laneId, ?string $artifactDirectory = null): void
|
||||
{
|
||||
$artifactPaths = self::artifactPaths($laneId, $artifactDirectory);
|
||||
|
||||
foreach (['report', 'summary', 'budget'] as $artifactKey) {
|
||||
$path = TestLaneManifest::absolutePath($artifactPaths[$artifactKey]);
|
||||
|
||||
if (is_dir($path) || (! is_file($path) && ! is_link($path))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! @unlink($path) && (is_file($path) || is_link($path))) {
|
||||
throw new RuntimeException(sprintf('Unable to invalidate stale test-lane artifact [%s].', $path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{provisional: string, final: string}> $artifactContents
|
||||
* @param (callable(string, string): void)|null $writer
|
||||
*/
|
||||
private static function commitFinalResultArtifacts(array $artifactContents, ?callable $writer = null): void
|
||||
{
|
||||
$finalWriter = $writer ?? static function (string $path, string $contents): void {
|
||||
self::writeRequiredFile($path, $contents);
|
||||
};
|
||||
|
||||
try {
|
||||
foreach ($artifactContents as $path => $contents) {
|
||||
$finalWriter($path, $contents['final']);
|
||||
}
|
||||
} catch (Throwable $throwable) {
|
||||
foreach ($artifactContents as $path => $contents) {
|
||||
if (self::writeFile($path, $contents['provisional'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_file($path) || is_link($path)) {
|
||||
// Best effort: remove a possibly final artifact when the
|
||||
// provisional rollback itself cannot be persisted.
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
private static function copyFile(string $sourcePath, string $targetPath): bool
|
||||
{
|
||||
return @copy($sourcePath, $targetPath) === true && is_file($targetPath);
|
||||
}
|
||||
|
||||
private static function writeFile(string $path, string $contents): bool
|
||||
{
|
||||
$writtenBytes = @file_put_contents($path, $contents);
|
||||
|
||||
return is_int($writtenBytes) && $writtenBytes === strlen($contents);
|
||||
}
|
||||
|
||||
private static function writeRequiredFile(string $path, string $contents): void
|
||||
{
|
||||
if (! self::writeFile($path, $contents)) {
|
||||
throw new RuntimeException(sprintf('Unable to write test-lane artifact [%s].', $path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,4 +54,47 @@
|
||||
'measuredSeconds',
|
||||
'budgetStatus',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when an active blocking profile is missing a required input', function (): void {
|
||||
$profile = TestLaneBudget::enforcementProfile('fast-feedback', 'pull-request');
|
||||
unset($profile['effectiveThresholdSeconds']);
|
||||
|
||||
expect(fn (): array => TestLaneBudget::evaluateTriggerAwareBudgetProfile($profile, 216.0))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('fails closed when an active blocking profile contains malformed inputs', function (): void {
|
||||
$profile = TestLaneBudget::enforcementProfile('fast-feedback', 'pull-request');
|
||||
$baseThresholdSeconds = (int) $profile['baseThresholdSeconds'];
|
||||
$varianceAllowanceSeconds = (int) $profile['varianceAllowanceSeconds'];
|
||||
$malformedProfiles = [
|
||||
array_replace($profile, ['laneId' => '']),
|
||||
array_replace($profile, ['baseThresholdSeconds' => 'not-numeric']),
|
||||
array_replace($profile, ['varianceAllowanceSeconds' => -1]),
|
||||
array_replace($profile, [
|
||||
'effectiveThresholdSeconds' => $baseThresholdSeconds + $varianceAllowanceSeconds + 1,
|
||||
]),
|
||||
array_replace($profile, ['enforcementMode' => 'soft-warn']),
|
||||
array_replace($profile, [
|
||||
'baseThresholdSeconds' => 999,
|
||||
'effectiveThresholdSeconds' => 999 + $varianceAllowanceSeconds,
|
||||
]),
|
||||
array_replace($profile, ['lifecycleState' => 'tampered']),
|
||||
array_replace($profile, ['triggerClass' => 'manual']),
|
||||
];
|
||||
|
||||
foreach ($malformedProfiles as $malformedProfile) {
|
||||
expect(fn (): array => TestLaneBudget::evaluateTriggerAwareBudgetProfile($malformedProfile, 216.0))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed when the measured time is non-finite or negative', function (): void {
|
||||
$profile = TestLaneBudget::enforcementProfile('fast-feedback', 'pull-request');
|
||||
|
||||
foreach ([NAN, INF, -0.001] as $measuredSeconds) {
|
||||
expect(fn (): array => TestLaneBudget::evaluateTriggerAwareBudgetProfile($profile, $measuredSeconds))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,8 +2,116 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Support\TestLaneManifest;
|
||||
use Tests\Support\TestLaneReport;
|
||||
|
||||
function spec439ReportArtifactDirectory(string $suffix): string
|
||||
{
|
||||
return 'storage/logs/spec439-test-lanes/'.$suffix.'-'.bin2hex(random_bytes(6));
|
||||
}
|
||||
|
||||
function spec439WriteJUnit(string $laneId, string $artifactDirectory, string $xml): string
|
||||
{
|
||||
$path = TestLaneManifest::absolutePath(TestLaneReport::artifactPaths($laneId, $artifactDirectory)['junit']);
|
||||
|
||||
if (! is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($path, $xml);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec439ReadLaneReport(string $laneId, string $artifactDirectory): array
|
||||
{
|
||||
$path = TestLaneManifest::absolutePath(TestLaneReport::artifactPaths($laneId, $artifactDirectory)['report']);
|
||||
|
||||
return json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
function spec439RemoveReportArtifacts(string $artifactDirectory): void
|
||||
{
|
||||
$directory = TestLaneManifest::absolutePath($artifactDirectory);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST,
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
||||
}
|
||||
|
||||
rmdir($directory);
|
||||
}
|
||||
|
||||
function spec439PassingJUnit(string $laneId = 'junit'): string
|
||||
{
|
||||
return sprintf(
|
||||
'<?xml version="1.0" encoding="UTF-8"?><testsuites tests="1" failures="0" errors="0" skipped="0"><testsuite name="%s" tests="1" failures="0" errors="0" skipped="0"><testcase name="passes" file="tests/Unit/Support/TestLaneReportTest.php" time="0.01"/></testsuite></testsuites>',
|
||||
$laneId,
|
||||
);
|
||||
}
|
||||
|
||||
function spec439RefreshLane(string $laneId, string $artifactDirectory): int
|
||||
{
|
||||
return spec439RefreshLaneResult($laneId, $artifactDirectory)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{int, array<string, mixed>}
|
||||
*/
|
||||
function spec439RefreshLaneResult(string $laneId, string $artifactDirectory): array
|
||||
{
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
$exitCode = TestLaneManifest::renderLatestReport($laneId, null, $artifactDirectory);
|
||||
$output = (string) ob_get_contents();
|
||||
|
||||
return [
|
||||
$exitCode,
|
||||
json_decode($output, true, 512, JSON_THROW_ON_ERROR),
|
||||
];
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TResult
|
||||
*
|
||||
* @param callable(): TResult $callback
|
||||
* @return TResult
|
||||
*/
|
||||
function spec439WithoutCiContext(callable $callback): mixed
|
||||
{
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
return $callback();
|
||||
} finally {
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
}
|
||||
|
||||
it('extracts at least the top ten slowest entries from a junit artifact', function (): void {
|
||||
$junitPath = sys_get_temp_dir().'/tenantatlas-test-lane-junit.xml';
|
||||
|
||||
@ -25,7 +133,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
file_put_contents($junitPath, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><testsuites>".implode('', $testcases).'</testsuites>');
|
||||
file_put_contents($junitPath, '<?xml version="1.0" encoding="UTF-8"?><testsuites>'.implode('', $testcases).'</testsuites>');
|
||||
|
||||
$parsed = TestLaneReport::parseJUnit($junitPath, 'junit');
|
||||
|
||||
@ -73,4 +181,640 @@
|
||||
->and($written['budget'])->toStartWith($artifactDirectory.'/')
|
||||
->and(file_exists(base_path($written['summary'])))->toBeTrue()
|
||||
->and(file_exists(base_path($written['budget'])))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
it('[EF-001] persists a failed execution and report refresh cannot replace it with success', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('failed-refresh');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 2.5,
|
||||
capturedOutput: 'synthetic failure',
|
||||
exitCode: 7,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
$storedBeforeRefresh = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
$refreshExit = spec439RefreshLane('junit', $artifactDirectory);
|
||||
$storedAfterRefresh = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
|
||||
expect($finalized['execution']['exitCode'] ?? null)->toBe(7)
|
||||
->and($finalized['finalResult']['status'] ?? null)->toBe('failed')
|
||||
->and($storedBeforeRefresh['execution']['exitCode'] ?? null)->toBe(7)
|
||||
->and($refreshExit)->toBe(7)
|
||||
->and($storedAfterRefresh['execution']['exitCode'] ?? null)->toBe(7)
|
||||
->and($storedAfterRefresh['finalResult']['status'] ?? null)->toBe('failed')
|
||||
->and($storedAfterRefresh['ciSummary']['finalExitCode'] ?? null)->toBe(7);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('[EF-002] keeps a valid passing execution and evidence result successful through refresh', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('passing-refresh');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
|
||||
[$finalized, $refreshExit, $stored] = spec439WithoutCiContext(function () use ($artifactDirectory): array {
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.5,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
$refreshExit = spec439RefreshLane('junit', $artifactDirectory);
|
||||
$stored = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
|
||||
return [$finalized, $refreshExit, $stored];
|
||||
});
|
||||
|
||||
expect($finalized['execution']['status'] ?? null)->toBe('passed')
|
||||
->and($finalized['evidence']['status'] ?? null)->toBe('passed')
|
||||
->and($finalized['finalResult']['status'] ?? null)->toBe('passed')
|
||||
->and($refreshExit)->toBe(0)
|
||||
->and($stored['finalResult']['exitCode'] ?? null)->toBe(0);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses to replace an unresolved failed report claim with a successful refresh', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('failed-claim-contradiction');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
|
||||
[$refreshExit, $refreshed] = spec439WithoutCiContext(function () use ($artifactDirectory): array {
|
||||
TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.5,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
$reportPath = TestLaneManifest::absolutePath(
|
||||
TestLaneReport::artifactPaths('junit', $artifactDirectory)['report'],
|
||||
);
|
||||
$stored = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
$stored['finalResult']['status'] = 'failed';
|
||||
$stored['finalResult']['exitCode'] = 1;
|
||||
$stored['ciSummary']['finalStatus'] = 'failed';
|
||||
$stored['ciSummary']['finalExitCode'] = 1;
|
||||
file_put_contents(
|
||||
$reportPath,
|
||||
json_encode($stored, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
|
||||
);
|
||||
|
||||
$refreshExit = spec439RefreshLane('junit', $artifactDirectory);
|
||||
$refreshed = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
|
||||
return [$refreshExit, $refreshed];
|
||||
});
|
||||
|
||||
expect($refreshExit)->toBe(1)
|
||||
->and($refreshed['execution']['exitCode'] ?? null)->toBe(0)
|
||||
->and($refreshed['evidence']['status'] ?? null)->toBe('passed')
|
||||
->and($refreshed['finalResult']['status'] ?? null)->toBe('failed')
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
'report-claim-contradiction',
|
||||
);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps unresolved workflow entry points blocking through finalization and contextless refresh', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('wrapper-failure');
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID=unknown-spec439-workflow');
|
||||
spec439WriteJUnit('fast-feedback', $artifactDirectory, spec439PassingJUnit('fast-feedback'));
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'fast-feedback',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
$refreshExit = spec439RefreshLane('fast-feedback', $artifactDirectory);
|
||||
$refreshed = spec439ReadLaneReport('fast-feedback', $artifactDirectory);
|
||||
|
||||
expect($finalized['ciContext']['entryPointResolved'] ?? null)->toBeFalse()
|
||||
->and($finalized['ciSummary']['primaryFailureClassId'] ?? null)->toBe('wrapper-failure')
|
||||
->and($finalized['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($finalized['finalResult']['failureContextIds'] ?? [])->toContain('wrapper-failure')
|
||||
->and($refreshExit)->toBe(1)
|
||||
->and($refreshed['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->toContain('wrapper-failure');
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps a known workflow bound to the wrong lane on the wrapper-failure final-result path', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('wrong-lane-wrapper-failure');
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID=pr-fast-feedback');
|
||||
spec439WriteJUnit('confidence', $artifactDirectory, spec439PassingJUnit('confidence'));
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'confidence',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
$refreshExit = spec439RefreshLane('confidence', $artifactDirectory);
|
||||
$refreshed = spec439ReadLaneReport('confidence', $artifactDirectory);
|
||||
|
||||
expect($finalized['ciContext']['entryPointResolved'] ?? null)->toBeTrue()
|
||||
->and($finalized['ciContext']['workflowLaneMatched'] ?? null)->toBeFalse()
|
||||
->and($finalized['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($finalized['finalResult']['failureContextIds'] ?? [])->toContain('wrapper-failure')
|
||||
->and($refreshExit)->toBe(1)
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->toContain('wrapper-failure');
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps an explicitly invalid current workflow blocking while supplementing only its stored trigger', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('invalid-current-workflow');
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID=pr-fast-feedback');
|
||||
spec439WriteJUnit('fast-feedback', $artifactDirectory, spec439PassingJUnit('fast-feedback'));
|
||||
|
||||
TestLaneReport::finalizeLane(
|
||||
laneId: 'fast-feedback',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID=unknown-current-workflow');
|
||||
[$refreshExit, $refreshed] = spec439RefreshLaneResult('fast-feedback', $artifactDirectory);
|
||||
|
||||
expect($refreshExit)->toBe(1)
|
||||
->and($refreshed['ciContext']['workflowId'] ?? null)->toBe('unknown-current-workflow')
|
||||
->and($refreshed['ciContext']['triggerClass'] ?? null)->toBe('pull-request')
|
||||
->and($refreshed['ciContext']['entryPointResolved'] ?? null)->toBeFalse()
|
||||
->and($refreshed['ciContext']['workflowLaneMatched'] ?? null)->toBeFalse()
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->toContain('wrapper-failure');
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
});
|
||||
|
||||
it('propagates canonical blocking and advisory budgets through finalization and refresh', function (): void {
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
$fixtures = [
|
||||
[
|
||||
'laneId' => 'fast-feedback',
|
||||
'triggerClass' => 'pull-request',
|
||||
'wallClockSeconds' => 216.0,
|
||||
'expectedExitCode' => 1,
|
||||
'expectedBlockingStatus' => 'blocking',
|
||||
'expectedFinalStatus' => 'failed',
|
||||
'expectedContextKey' => 'failureContextIds',
|
||||
'expectedContextId' => 'blocking-budget-breach',
|
||||
],
|
||||
[
|
||||
'laneId' => 'confidence',
|
||||
'triggerClass' => 'mainline-push',
|
||||
'wallClockSeconds' => 481.0,
|
||||
'expectedExitCode' => 0,
|
||||
'expectedBlockingStatus' => 'non-blocking-warning',
|
||||
'expectedFinalStatus' => 'passed',
|
||||
'expectedContextKey' => 'advisoryContextIds',
|
||||
'expectedContextId' => 'budget-advisory-warning',
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
foreach ($fixtures as $fixture) {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('budget-'.$fixture['laneId']);
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$fixture['triggerClass']);
|
||||
spec439WriteJUnit(
|
||||
$fixture['laneId'],
|
||||
$artifactDirectory,
|
||||
spec439PassingJUnit($fixture['laneId']),
|
||||
);
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: $fixture['laneId'],
|
||||
wallClockSeconds: $fixture['wallClockSeconds'],
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$refreshExit = spec439RefreshLane($fixture['laneId'], $artifactDirectory);
|
||||
$refreshed = spec439ReadLaneReport($fixture['laneId'], $artifactDirectory);
|
||||
$summaryPath = TestLaneManifest::absolutePath(
|
||||
TestLaneReport::artifactPaths($fixture['laneId'], $artifactDirectory)['summary'],
|
||||
);
|
||||
$summary = (string) file_get_contents($summaryPath);
|
||||
|
||||
expect($finalized['ciBudgetEvaluation']['budgetStatus'] ?? null)->toBe('over-budget')
|
||||
->and($finalized['finalResult']['exitCode'] ?? null)->toBe($fixture['expectedExitCode'])
|
||||
->and($finalized['finalResult']['blockingStatus'] ?? null)->toBe($fixture['expectedBlockingStatus'])
|
||||
->and($finalized['finalResult'][$fixture['expectedContextKey']] ?? [])->toContain(
|
||||
$fixture['expectedContextId'],
|
||||
)
|
||||
->and($finalized['ciSummary']['finalExitCode'] ?? null)->toBe($fixture['expectedExitCode'])
|
||||
->and($refreshExit)->toBe($fixture['expectedExitCode'])
|
||||
->and($refreshed['ciBudgetEvaluation']['budgetStatus'] ?? null)->toBe('over-budget')
|
||||
->and($refreshed['finalResult']['exitCode'] ?? null)->toBe($fixture['expectedExitCode'])
|
||||
->and($refreshed['finalResult']['blockingStatus'] ?? null)->toBe($fixture['expectedBlockingStatus'])
|
||||
->and($refreshed['finalResult'][$fixture['expectedContextKey']] ?? [])->toContain(
|
||||
$fixture['expectedContextId'],
|
||||
)
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->not->toContain(
|
||||
'report-claim-contradiction',
|
||||
)
|
||||
->and($summary)->toContain(sprintf(
|
||||
'- CI outcome: %s / %s',
|
||||
$fixture['expectedFinalStatus'],
|
||||
$fixture['expectedBlockingStatus'],
|
||||
));
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
});
|
||||
|
||||
it('classifies JUnit existence, validity, counts, failures, errors, and skipped evidence', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('junit-evidence');
|
||||
|
||||
try {
|
||||
$missingPath = TestLaneManifest::absolutePath($artifactDirectory.'/missing.xml');
|
||||
$malformedPath = TestLaneManifest::absolutePath($artifactDirectory.'/malformed.xml');
|
||||
$failurePath = TestLaneManifest::absolutePath($artifactDirectory.'/failure.xml');
|
||||
$errorPath = TestLaneManifest::absolutePath($artifactDirectory.'/error.xml');
|
||||
$skippedPath = TestLaneManifest::absolutePath($artifactDirectory.'/skipped.xml');
|
||||
mkdir(dirname($malformedPath), 0777, true);
|
||||
file_put_contents($malformedPath, '<testsuites><broken>');
|
||||
file_put_contents($failurePath, '<?xml version="1.0"?><testsuites><testsuite name="failure"><testcase name="fails" file="failure.php" time="0.1"><failure message="nope"/></testcase></testsuite></testsuites>');
|
||||
file_put_contents($errorPath, '<?xml version="1.0"?><testsuites><testsuite name="error"><testcase name="errors" file="error.php" time="0.1"><error message="boom"/></testcase></testsuite></testsuites>');
|
||||
file_put_contents($skippedPath, '<?xml version="1.0"?><testsuites><testsuite name="skipped"><testcase name="skips" file="skipped.php" time="0"><skipped/></testcase></testsuite></testsuites>');
|
||||
|
||||
$missing = TestLaneReport::parseJUnit($missingPath, 'junit')['evidence'];
|
||||
$malformed = TestLaneReport::parseJUnit($malformedPath, 'junit')['evidence'];
|
||||
$failure = TestLaneReport::parseJUnit($failurePath, 'junit')['evidence'];
|
||||
$error = TestLaneReport::parseJUnit($errorPath, 'junit')['evidence'];
|
||||
$skipped = TestLaneReport::parseJUnit($skippedPath, 'junit')['evidence'];
|
||||
|
||||
expect($missing)->toMatchArray(['exists' => false, 'parseValid' => false, 'status' => 'missing'])
|
||||
->and($malformed)->toMatchArray(['exists' => true, 'parseValid' => false, 'status' => 'malformed'])
|
||||
->and($failure)->toMatchArray(['testCount' => 1, 'failureCount' => 1, 'errorCount' => 0, 'status' => 'failed'])
|
||||
->and($error)->toMatchArray(['testCount' => 1, 'failureCount' => 0, 'errorCount' => 1, 'status' => 'error-containing'])
|
||||
->and($skipped)->toMatchArray(['testCount' => 1, 'skippedCount' => 1, 'status' => 'skipped'])
|
||||
->and($failure['path'])->toBe($failurePath);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('[EF-005] makes passed execution plus failing or error JUnit contradictions visible and non-zero', function (): void {
|
||||
foreach ([
|
||||
'failure' => ['status' => 'failed', 'contextId' => 'junit-failures'],
|
||||
'error' => ['status' => 'error-containing', 'contextId' => 'junit-errors'],
|
||||
] as $element => $expected) {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('junit-'.$element.'-contradiction');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit(
|
||||
'junit',
|
||||
$artifactDirectory,
|
||||
sprintf(
|
||||
'<?xml version="1.0"?><testsuites><testsuite name="%1$s"><testcase name="fails" file="%1$s.php" time="0.1"><%1$s message="nope"/></testcase></testsuite></testsuites>',
|
||||
$element,
|
||||
),
|
||||
);
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
expect($finalized['evidence']['status'] ?? null)->toBe($expected['status'])
|
||||
->and($finalized['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($finalized['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
$expected['contextId'],
|
||||
'evidence-contradiction',
|
||||
);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('[EF-006] fails closed for missing and malformed required JUnit evidence', function (): void {
|
||||
foreach (['missing', 'malformed'] as $fixture) {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('required-'.$fixture);
|
||||
|
||||
try {
|
||||
if ($fixture === 'malformed') {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, '<testsuites><broken>');
|
||||
}
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
expect($finalized['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($finalized['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
$fixture === 'missing' ? 'evidence-missing' : 'evidence-malformed',
|
||||
);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed when report refresh starts from malformed report evidence', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('malformed-report');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
$reportPath = TestLaneManifest::absolutePath(TestLaneReport::artifactPaths('junit', $artifactDirectory)['report']);
|
||||
file_put_contents($reportPath, '{not-json');
|
||||
|
||||
$refreshExit = spec439RefreshLane('junit', $artifactDirectory);
|
||||
$stored = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
|
||||
expect($refreshExit)->toBe(1)
|
||||
->and($stored['reportInput']['status'] ?? null)->toBe('malformed')
|
||||
->and($stored['finalResult']['failureContextIds'] ?? [])->toContain('report-input-malformed');
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('classifies scalar and list report roots as malformed while retaining a minimal legacy object diagnostically', function (): void {
|
||||
foreach ([
|
||||
'scalar' => ['json' => '"legacy-pass"', 'expectedStatus' => 'malformed'],
|
||||
'list' => ['json' => '[{"laneId":"junit"}]', 'expectedStatus' => 'malformed'],
|
||||
'legacy-object' => ['json' => '{"laneId":"junit","wallClockSeconds":1.0}', 'expectedStatus' => 'valid'],
|
||||
] as $fixture => $expectation) {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('report-shape-'.$fixture);
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
$reportPath = TestLaneManifest::absolutePath(TestLaneReport::artifactPaths('junit', $artifactDirectory)['report']);
|
||||
file_put_contents($reportPath, $expectation['json']);
|
||||
|
||||
[$refreshExit, $refreshed] = spec439WithoutCiContext(
|
||||
fn (): array => spec439RefreshLaneResult('junit', $artifactDirectory),
|
||||
);
|
||||
|
||||
expect($refreshed['reportInput']['status'] ?? null)->toBe($expectation['expectedStatus'])
|
||||
->and($refreshExit)->toBe(1)
|
||||
->and($refreshed['finalResult']['status'] ?? null)->toBe('failed');
|
||||
|
||||
if ($expectation['expectedStatus'] === 'malformed') {
|
||||
expect($refreshed['finalResult']['failureContextIds'] ?? [])->toContain('report-input-malformed');
|
||||
}
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns current failure truth when the required report artifact cannot be written', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('report-write-failure');
|
||||
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
|
||||
|
||||
try {
|
||||
putenv('TENANTATLAS_CI_TRIGGER_CLASS');
|
||||
putenv('TENANTATLAS_CI_WORKFLOW_ID=unknown-write-workflow');
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
$reportPath = TestLaneManifest::absolutePath(TestLaneReport::artifactPaths('junit', $artifactDirectory)['report']);
|
||||
mkdir($reportPath, 0777, true);
|
||||
|
||||
$finalized = TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
);
|
||||
|
||||
expect($finalized['reportGeneration']['status'] ?? null)->toBe('failed')
|
||||
->and($finalized['reportGeneration']['errorClass'] ?? null)->toBe(RuntimeException::class)
|
||||
->and($finalized['finalResult']['exitCode'] ?? null)->toBe(1)
|
||||
->and($finalized['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
'report-generation-failure',
|
||||
'artifact-publication-failure',
|
||||
'wrapper-failure',
|
||||
)
|
||||
->and($finalized['ciSummary']['primaryFailureClassId'] ?? null)->toBe('wrapper-failure');
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
$originalTriggerClass === false
|
||||
? putenv('TENANTATLAS_CI_TRIGGER_CLASS')
|
||||
: putenv('TENANTATLAS_CI_TRIGGER_CLASS='.$originalTriggerClass);
|
||||
$originalWorkflowId === false
|
||||
? putenv('TENANTATLAS_CI_WORKFLOW_ID')
|
||||
: putenv('TENANTATLAS_CI_WORKFLOW_ID='.$originalWorkflowId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps report-generation and artifact-publication failures visible when refresh cannot write a required artifact', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('refresh-write-failure');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
spec439WithoutCiContext(fn (): array => TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
));
|
||||
|
||||
$summaryPath = TestLaneManifest::absolutePath(
|
||||
TestLaneReport::artifactPaths('junit', $artifactDirectory)['summary'],
|
||||
);
|
||||
$reportPath = TestLaneManifest::absolutePath(
|
||||
TestLaneReport::artifactPaths('junit', $artifactDirectory)['report'],
|
||||
);
|
||||
unlink($summaryPath);
|
||||
mkdir($summaryPath);
|
||||
|
||||
[$refreshExit, $refreshed] = spec439WithoutCiContext(
|
||||
fn (): array => spec439RefreshLaneResult('junit', $artifactDirectory),
|
||||
);
|
||||
|
||||
expect($refreshExit)->toBe(1)
|
||||
->and($refreshed['reportGeneration']['status'] ?? null)->toBe('failed')
|
||||
->and($refreshed['artifactPublication']['complete'] ?? null)->toBeFalse()
|
||||
->and($refreshed['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
'report-generation-failure',
|
||||
'artifact-publication-failure',
|
||||
)
|
||||
->and($refreshed['ciSummary']['artifactStatus'] ?? null)->toBe('incomplete')
|
||||
->and(is_file($reportPath))->toBeFalse();
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the persisted report fail-closed when publication stops after the provisional report write', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('provisional-report-write-failure');
|
||||
|
||||
try {
|
||||
spec439WriteJUnit('junit', $artifactDirectory, spec439PassingJUnit());
|
||||
$artifactPaths = TestLaneReport::artifactPaths('junit', $artifactDirectory);
|
||||
$trendHistoryPath = TestLaneManifest::absolutePath($artifactPaths['trendHistory']);
|
||||
mkdir($trendHistoryPath, 0777, true);
|
||||
|
||||
$finalized = spec439WithoutCiContext(fn (): array => TestLaneReport::finalizeLane(
|
||||
laneId: 'junit',
|
||||
wallClockSeconds: 1.0,
|
||||
exitCode: 0,
|
||||
artifactDirectory: $artifactDirectory,
|
||||
));
|
||||
$persisted = spec439ReadLaneReport('junit', $artifactDirectory);
|
||||
|
||||
expect($finalized['reportGeneration']['status'] ?? null)->toBe('failed')
|
||||
->and($finalized['finalResult']['status'] ?? null)->toBe('failed')
|
||||
->and($persisted['reportGeneration']['status'] ?? null)->toBe('failed')
|
||||
->and($persisted['finalResult']['status'] ?? null)->toBe('failed')
|
||||
->and($persisted['finalResult']['failureContextIds'] ?? [])->toContain(
|
||||
'report-generation-failure',
|
||||
'artifact-publication-failure',
|
||||
);
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('rolls every result artifact back to provisional failure truth when a final write fails', function (): void {
|
||||
$artifactDirectory = spec439ReportArtifactDirectory('final-result-rollback');
|
||||
$absoluteDirectory = TestLaneManifest::absolutePath($artifactDirectory);
|
||||
$artifactContents = [
|
||||
$absoluteDirectory.'/summary.md' => [
|
||||
'provisional' => 'summary failed',
|
||||
'final' => 'summary passed',
|
||||
],
|
||||
$absoluteDirectory.'/budget.json' => [
|
||||
'provisional' => '{"finalResult":"failed"}',
|
||||
'final' => '{"finalResult":"passed"}',
|
||||
],
|
||||
$absoluteDirectory.'/report.json' => [
|
||||
'provisional' => '{"finalResult":"failed"}',
|
||||
'final' => '{"finalResult":"passed"}',
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
mkdir($absoluteDirectory, 0777, true);
|
||||
$commit = new ReflectionMethod(TestLaneReport::class, 'commitFinalResultArtifacts');
|
||||
$commit->setAccessible(true);
|
||||
|
||||
foreach ([1, 2, 3] as $failingWrite) {
|
||||
foreach ($artifactContents as $path => $contents) {
|
||||
file_put_contents($path, $contents['provisional']);
|
||||
}
|
||||
|
||||
$writeCount = 0;
|
||||
$writer = static function (string $path, string $contents) use (&$writeCount, $failingWrite): void {
|
||||
$writeCount++;
|
||||
|
||||
if ($writeCount === $failingWrite) {
|
||||
throw new RuntimeException('synthetic final publication failure');
|
||||
}
|
||||
|
||||
file_put_contents($path, $contents);
|
||||
};
|
||||
|
||||
expect(static fn () => $commit->invoke(null, $artifactContents, $writer))
|
||||
->toThrow(RuntimeException::class, 'synthetic final publication failure');
|
||||
|
||||
foreach ($artifactContents as $path => $contents) {
|
||||
expect((string) file_get_contents($path))->toBe($contents['provisional']);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
spec439RemoveReportArtifacts($artifactDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
it('[EF-007] preserves both test and report-generation failure contexts', function (): void {
|
||||
$result = TestLaneReport::deriveFinalResult(
|
||||
execution: TestLaneReport::executionResult(9),
|
||||
evidence: [
|
||||
'path' => '/tmp/passed.xml',
|
||||
'exists' => true,
|
||||
'parseValid' => true,
|
||||
'testCount' => 1,
|
||||
'failureCount' => 0,
|
||||
'errorCount' => 0,
|
||||
'skippedCount' => 0,
|
||||
'status' => 'passed',
|
||||
],
|
||||
reportGeneration: [
|
||||
'status' => 'failed',
|
||||
'errorClass' => RuntimeException::class,
|
||||
'message' => 'synthetic report write failure',
|
||||
],
|
||||
);
|
||||
|
||||
expect($result['status'])->toBe('failed')
|
||||
->and($result['exitCode'])->toBe(9)
|
||||
->and($result['failureContextIds'])->toContain(
|
||||
'test-failure',
|
||||
'report-generation-failure',
|
||||
'evidence-contradiction',
|
||||
);
|
||||
});
|
||||
|
||||
@ -71,15 +71,32 @@ ## Ongoing Design Coverage Gate
|
||||
completion gate, browser/no-browser proof, human sanity review, surface
|
||||
budgets, Technical Annex demotion, and implementation-report requirements.
|
||||
|
||||
The mechanical PR guard is `scripts/check-ui-productization-coverage`. The PR fast-feedback workflow runs it against `origin/dev`: if guarded UI surface paths change under `apps/platform/app/Filament/`, `apps/platform/app/Support/Navigation/`, `apps/platform/app/Providers/Filament/`, `apps/platform/resources/views/`, `apps/platform/app/Livewire/`, or `apps/platform/routes/`, the PR must also update a real UI coverage artifact, or the active spec must contain a checked UI impact decision or a checked `- [x] No UI surface impact` decision with nearby non-empty rationale.
|
||||
The mechanical PR guard is `scripts/check-ui-productization-coverage`. It resolves
|
||||
the branch family first, validates a matching concrete ref, and uses the merge
|
||||
base for feature-relative changes. The checked-in PR fast-feedback workflow
|
||||
passes `${{ github.base_ref }}` as the actual PR target, fetches the matching
|
||||
remote ref with complete checkout history, and fails closed when the target is
|
||||
missing or family-inconsistent. Repository contracts prove that declaration;
|
||||
live Gitea resolution of the event value remains an external verification
|
||||
residual. If guarded UI surface paths change under `apps/platform/app/Filament/`,
|
||||
`apps/platform/app/Support/Navigation/`,
|
||||
`apps/platform/app/Providers/Filament/`, `apps/platform/resources/views/`,
|
||||
`apps/platform/app/Livewire/`, or `apps/platform/routes/`, the PR must also
|
||||
update a real UI coverage artifact, or the active spec must contain a checked UI
|
||||
impact decision or a checked `- [x] No UI surface impact` decision with nearby
|
||||
non-empty rationale.
|
||||
|
||||
Standalone usage:
|
||||
|
||||
```bash
|
||||
bash scripts/check-ui-productization-coverage HEAD
|
||||
bash scripts/check-ui-productization-coverage platform-dev --branch-family=platform
|
||||
```
|
||||
|
||||
The guard checks committed changes against the supplied base ref and also considers staged, unstaged, and untracked local files where Git can report them. It is a silent-omission guard, not a design-review substitute.
|
||||
Use `website-dev --branch-family=website` for ordinary website work. An active
|
||||
spec with unambiguous branch metadata may omit both arguments. The guard checks
|
||||
committed changes against the resolver's merge-base range and also considers
|
||||
staged, unstaged, and untracked local files where Git can report them. It is a
|
||||
silent-omission guard, not a design-review substitute.
|
||||
|
||||
Unchecked template checkboxes, unchecked spec checkboxes, headings, and generic phrases in templates/prompts/docs do not satisfy the guard when guarded UI files changed. Template and prompt changes remain allowed without coverage acknowledgment when no guarded UI file changed.
|
||||
|
||||
|
||||
@ -4,32 +4,74 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BASE_REF="${1:-${GITHUB_BASE_REF:-${GITEA_BASE_REF:-}}}"
|
||||
RESOLVER="${SCRIPT_DIR}/resolve-feature-base"
|
||||
EXPLICIT_REF=""
|
||||
|
||||
if [[ -z "${BASE_REF}" ]]; then
|
||||
if git -C "${ROOT_DIR}" rev-parse --verify origin/dev >/dev/null 2>&1; then
|
||||
BASE_REF="origin/dev"
|
||||
elif git -C "${ROOT_DIR}" rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
||||
BASE_REF="HEAD~1"
|
||||
else
|
||||
echo "UI/Productization Coverage guard: no base ref supplied and no fallback ref exists." >&2
|
||||
echo "Usage: scripts/check-ui-productization-coverage <base-ref>" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ $# -gt 0 && "$1" != --* ]]; then
|
||||
EXPLICIT_REF="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
if ! git -C "${ROOT_DIR}" rev-parse --verify "${BASE_REF}^{commit}" >/dev/null 2>&1; then
|
||||
echo "UI/Productization Coverage guard: base ref '${BASE_REF}' is not available." >&2
|
||||
echo "Fetch the target branch first or pass an available commit/ref." >&2
|
||||
if [[ ! -x "${RESOLVER}" ]]; then
|
||||
echo "UI/Productization Coverage guard: executable resolver is unavailable at ${RESOLVER}." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if BASE_COMMIT="$(git -C "${ROOT_DIR}" merge-base "${BASE_REF}" HEAD 2>/dev/null)"; then
|
||||
BASE_RANGE="${BASE_COMMIT}...HEAD"
|
||||
else
|
||||
BASE_RANGE="${BASE_REF}..HEAD"
|
||||
resolver_arguments=(--format=kv)
|
||||
|
||||
if [[ -n "${EXPLICIT_REF}" ]]; then
|
||||
resolver_arguments+=("--ref=${EXPLICIT_REF}")
|
||||
fi
|
||||
|
||||
resolver_arguments+=("$@")
|
||||
|
||||
set +e
|
||||
RESOLVER_OUTPUT="$(
|
||||
cd "${ROOT_DIR}"
|
||||
"${RESOLVER}" "${resolver_arguments[@]}" 2>&1
|
||||
)"
|
||||
RESOLVER_STATUS=$?
|
||||
set -e
|
||||
|
||||
if [[ ${RESOLVER_STATUS} -ne 0 ]]; then
|
||||
printf '%s\n' "${RESOLVER_OUTPUT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
RESOLUTION_STATUS=""
|
||||
BRANCH_FAMILY=""
|
||||
EXPECTED_BRANCH=""
|
||||
SELECTED_REF=""
|
||||
TARGET_COMMIT=""
|
||||
MERGE_BASE=""
|
||||
BASE_RANGE=""
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
case "${key}" in
|
||||
status) RESOLUTION_STATUS="${value}" ;;
|
||||
branch_family) BRANCH_FAMILY="${value}" ;;
|
||||
expected_branch) EXPECTED_BRANCH="${value}" ;;
|
||||
selected_ref) SELECTED_REF="${value}" ;;
|
||||
target_commit) TARGET_COMMIT="${value}" ;;
|
||||
merge_base) MERGE_BASE="${value}" ;;
|
||||
base_range) BASE_RANGE="${value}" ;;
|
||||
esac
|
||||
done <<< "${RESOLVER_OUTPUT}"
|
||||
|
||||
if [[ "${RESOLUTION_STATUS}" != "resolved" \
|
||||
|| -z "${BRANCH_FAMILY}" \
|
||||
|| -z "${EXPECTED_BRANCH}" \
|
||||
|| -z "${SELECTED_REF}" \
|
||||
|| -z "${TARGET_COMMIT}" \
|
||||
|| -z "${MERGE_BASE}" \
|
||||
|| -z "${BASE_RANGE}" ]]; then
|
||||
echo "UI/Productization Coverage guard: resolver returned an incomplete success contract." >&2
|
||||
printf '%s\n' "${RESOLVER_OUTPUT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s\n' "${RESOLVER_OUTPUT}"
|
||||
|
||||
collect_changed_files() {
|
||||
git -C "${ROOT_DIR}" diff --name-only --diff-filter=ACDMRT "${BASE_RANGE}" 2>/dev/null || true
|
||||
git -C "${ROOT_DIR}" diff --cached --name-only --diff-filter=ACDMRT 2>/dev/null || true
|
||||
|
||||
@ -77,22 +77,26 @@ if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LAN
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${WORKFLOW_ID}" ]]; then
|
||||
export TENANTATLAS_CI_WORKFLOW_ID="${WORKFLOW_ID}"
|
||||
fi
|
||||
|
||||
if [[ -n "${TRIGGER_CLASS}" ]]; then
|
||||
export TENANTATLAS_CI_TRIGGER_CLASS="${TRIGGER_CLASS}"
|
||||
fi
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
sail_composer=(
|
||||
./vendor/bin/sail
|
||||
run
|
||||
env
|
||||
"TENANTATLAS_CI_WORKFLOW_ID=${WORKFLOW_ID}"
|
||||
"TENANTATLAS_CI_TRIGGER_CLASS=${TRIGGER_CLASS}"
|
||||
composer
|
||||
run
|
||||
--timeout=0
|
||||
"${COMPOSER_SCRIPT}"
|
||||
)
|
||||
|
||||
if [[ ${#remaining_args[@]} -gt 0 ]]; then
|
||||
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "${remaining_args[@]}"
|
||||
"${sail_composer[@]}" -- "${remaining_args[@]}"
|
||||
else
|
||||
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
|
||||
"${sail_composer[@]}"
|
||||
fi
|
||||
|
||||
if [[ "${CAPTURE_BASELINE}" == true ]]; then
|
||||
copy_heavy_baseline_artifacts
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -99,14 +99,6 @@ if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LAN
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${WORKFLOW_ID}" ]]; then
|
||||
export TENANTATLAS_CI_WORKFLOW_ID="${WORKFLOW_ID}"
|
||||
fi
|
||||
|
||||
if [[ -n "${TRIGGER_CLASS}" ]]; then
|
||||
export TENANTATLAS_CI_TRIGGER_CLASS="${TRIGGER_CLASS}"
|
||||
fi
|
||||
|
||||
trend_history_target_path() {
|
||||
echo "${APP_DIR}/storage/logs/test-lanes/${LANE}-latest.trend-history.json"
|
||||
}
|
||||
@ -352,7 +344,19 @@ cd "${APP_DIR}"
|
||||
|
||||
hydrate_trend_history
|
||||
|
||||
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
|
||||
sail_composer=(
|
||||
./vendor/bin/sail
|
||||
run
|
||||
env
|
||||
"TENANTATLAS_CI_WORKFLOW_ID=${WORKFLOW_ID}"
|
||||
"TENANTATLAS_CI_TRIGGER_CLASS=${TRIGGER_CLASS}"
|
||||
composer
|
||||
run
|
||||
--timeout=0
|
||||
"${COMPOSER_SCRIPT}"
|
||||
)
|
||||
|
||||
"${sail_composer[@]}"
|
||||
|
||||
case "${LANE}" in
|
||||
heavy-governance|browser)
|
||||
|
||||
485
scripts/resolve-feature-base
Executable file
485
scripts/resolve-feature-base
Executable file
@ -0,0 +1,485 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
format="human"
|
||||
cli_family=""
|
||||
work_classification=""
|
||||
pull_request_target=""
|
||||
spec_path=""
|
||||
branch_name=""
|
||||
repo_scope=""
|
||||
explicit_ref=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/resolve-feature-base [options]
|
||||
|
||||
Options:
|
||||
--branch-family=platform|website|repository-wide
|
||||
--work-classification=platform|website|cross-stream|promotion|repository-governance
|
||||
--pull-request-target=<platform-dev|website-dev|dev>
|
||||
--spec=<path-to-spec.md>
|
||||
--branch-name=<feature-branch>
|
||||
--repo-scope=platform|website|repository
|
||||
--ref=<family-matching-git-ref>
|
||||
--format=human|kv
|
||||
EOF
|
||||
}
|
||||
|
||||
fail_resolution() {
|
||||
local reason="$1"
|
||||
shift
|
||||
|
||||
if [[ "${format}" == "human" ]]; then
|
||||
echo "Feature base resolution failed." >&2
|
||||
fi
|
||||
|
||||
echo "status=failed" >&2
|
||||
echo "reason=${reason}" >&2
|
||||
|
||||
for detail in "$@"; do
|
||||
echo "${detail}" >&2
|
||||
done
|
||||
|
||||
echo "guidance=Declare a consistent branch family, work classification, PR target, active spec, and family-matching ref." >&2
|
||||
echo "diff_ran=false" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--branch-family=*)
|
||||
cli_family="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--branch-family)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--branch-family"
|
||||
cli_family="$2"
|
||||
shift 2
|
||||
;;
|
||||
--work-classification=*)
|
||||
work_classification="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--work-classification)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--work-classification"
|
||||
work_classification="$2"
|
||||
shift 2
|
||||
;;
|
||||
--pull-request-target=*)
|
||||
pull_request_target="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--pull-request-target)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--pull-request-target"
|
||||
pull_request_target="$2"
|
||||
shift 2
|
||||
;;
|
||||
--spec=*)
|
||||
spec_path="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--spec)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--spec"
|
||||
spec_path="$2"
|
||||
shift 2
|
||||
;;
|
||||
--branch-name=*)
|
||||
branch_name="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--branch-name)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--branch-name"
|
||||
branch_name="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo-scope=*)
|
||||
repo_scope="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--repo-scope)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--repo-scope"
|
||||
repo_scope="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ref=*)
|
||||
explicit_ref="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ref)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--ref"
|
||||
explicit_ref="$2"
|
||||
shift 2
|
||||
;;
|
||||
--format=*)
|
||||
format="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--format)
|
||||
[[ $# -ge 2 ]] || fail_resolution "missing-option-value" "option=--format"
|
||||
format="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail_resolution "unknown-option" "option=$1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${format}" != "human" && "${format}" != "kv" ]]; then
|
||||
fail_resolution "invalid-format" "format=${format}"
|
||||
fi
|
||||
|
||||
normalize_family() {
|
||||
case "$1" in
|
||||
platform)
|
||||
echo "platform"
|
||||
;;
|
||||
website)
|
||||
echo "website"
|
||||
;;
|
||||
repository-wide)
|
||||
echo "repository-wide"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
family_for_branch() {
|
||||
case "$1" in
|
||||
platform-dev|origin/platform-dev|refs/heads/platform-dev|refs/remotes/origin/platform-dev)
|
||||
echo "platform"
|
||||
;;
|
||||
website-dev|origin/website-dev|refs/heads/website-dev|refs/remotes/origin/website-dev)
|
||||
echo "website"
|
||||
;;
|
||||
dev|origin/dev|refs/heads/dev|refs/remotes/origin/dev)
|
||||
echo "repository-wide"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
expected_branch_for_family() {
|
||||
case "$1" in
|
||||
platform)
|
||||
echo "platform-dev"
|
||||
;;
|
||||
website)
|
||||
echo "website-dev"
|
||||
;;
|
||||
repository-wide)
|
||||
echo "dev"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
trim_value() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
value="${value//\`/}"
|
||||
printf '%s\n' "${value}"
|
||||
}
|
||||
|
||||
metadata_value() {
|
||||
local file="$1"
|
||||
local label="$2"
|
||||
local line
|
||||
|
||||
line="$(awk -v prefix="**${label}**:" 'index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }' "${file}")"
|
||||
trim_value "${line}"
|
||||
}
|
||||
|
||||
repository_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "${repository_root}" ]]; then
|
||||
fail_resolution "not-a-git-repository"
|
||||
fi
|
||||
|
||||
if [[ -z "${branch_name}" ]]; then
|
||||
branch_name="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
environment_family="${TENANTATLAS_BRANCH_FAMILY:-}"
|
||||
if [[ -n "${cli_family}" && -n "${environment_family}" && "${cli_family}" != "${environment_family}" ]]; then
|
||||
fail_resolution "branch-family-conflict" "cli_family=${cli_family}" "workflow_family=${environment_family}"
|
||||
fi
|
||||
|
||||
explicit_family_raw="${cli_family:-${environment_family}}"
|
||||
explicit_family=""
|
||||
if [[ -n "${explicit_family_raw}" ]]; then
|
||||
explicit_family="$(normalize_family "${explicit_family_raw}" || true)"
|
||||
if [[ -z "${explicit_family}" ]]; then
|
||||
fail_resolution "invalid-branch-family" "explicit=${explicit_family_raw}"
|
||||
fi
|
||||
fi
|
||||
|
||||
environment_classification="${TENANTATLAS_WORK_CLASSIFICATION:-}"
|
||||
if [[ -n "${work_classification}" && -n "${environment_classification}" && "${work_classification}" != "${environment_classification}" ]]; then
|
||||
fail_resolution "work-classification-conflict" "cli_classification=${work_classification}" "workflow_classification=${environment_classification}"
|
||||
fi
|
||||
work_classification="${work_classification:-${environment_classification}}"
|
||||
|
||||
case "${work_classification}" in
|
||||
""|platform|website|cross-stream|promotion|repository-governance)
|
||||
;;
|
||||
*)
|
||||
fail_resolution "invalid-work-classification" "work_classification=${work_classification}"
|
||||
;;
|
||||
esac
|
||||
|
||||
environment_ref="${TENANTATLAS_BASE_REF:-}"
|
||||
if [[ -n "${explicit_ref}" && -n "${environment_ref}" && "${explicit_ref}" != "${environment_ref}" ]]; then
|
||||
fail_resolution "explicit-ref-conflict" "cli_ref=${explicit_ref}" "workflow_ref=${environment_ref}"
|
||||
fi
|
||||
explicit_ref="${explicit_ref:-${environment_ref}}"
|
||||
|
||||
resolved_target=""
|
||||
target_family=""
|
||||
for target_signal in \
|
||||
"${pull_request_target}" \
|
||||
"${TENANTATLAS_PR_TARGET:-}" \
|
||||
"${GITHUB_BASE_REF:-}" \
|
||||
"${GITEA_BASE_REF:-}"; do
|
||||
[[ -z "${target_signal}" ]] && continue
|
||||
|
||||
signal_family="$(family_for_branch "${target_signal}" || true)"
|
||||
if [[ -z "${signal_family}" ]]; then
|
||||
fail_resolution "unknown-pull-request-target" "pull_request_target=${target_signal}"
|
||||
fi
|
||||
|
||||
if [[ -z "${resolved_target}" ]]; then
|
||||
resolved_target="${target_signal}"
|
||||
target_family="${signal_family}"
|
||||
elif [[ "${target_family}" != "${signal_family}" ]]; then
|
||||
fail_resolution "pull-request-target-conflict" "pull_request_targets=${resolved_target},${target_signal}"
|
||||
fi
|
||||
done
|
||||
|
||||
explicit_spec_requested=false
|
||||
if [[ -n "${spec_path}" ]]; then
|
||||
explicit_spec_requested=true
|
||||
elif [[ -n "${TENANTATLAS_ACTIVE_SPEC:-}" ]]; then
|
||||
spec_path="${TENANTATLAS_ACTIVE_SPEC}"
|
||||
explicit_spec_requested=true
|
||||
elif [[ "${branch_name}" == feat/* ]]; then
|
||||
candidate_spec="${repository_root}/specs/${branch_name#feat/}/spec.md"
|
||||
if [[ -f "${candidate_spec}" ]]; then
|
||||
spec_path="${candidate_spec}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${spec_path}" && "${spec_path}" != /* ]]; then
|
||||
spec_path="${repository_root}/${spec_path}"
|
||||
fi
|
||||
|
||||
if [[ "${explicit_spec_requested}" == true && ! -f "${spec_path}" ]]; then
|
||||
fail_resolution "spec-not-readable" "spec_path=${spec_path}"
|
||||
fi
|
||||
|
||||
spec_family=""
|
||||
spec_classification=""
|
||||
spec_declared_target=""
|
||||
if [[ -n "${spec_path}" && -f "${spec_path}" ]]; then
|
||||
feature_base="$(metadata_value "${spec_path}" "Feature Base")"
|
||||
merge_target="$(metadata_value "${spec_path}" "Normal Merge Target")"
|
||||
spec_declared_target="${merge_target}"
|
||||
development_stream="$(metadata_value "${spec_path}" "Development Stream")"
|
||||
|
||||
for metadata_branch in "${feature_base}" "${merge_target}"; do
|
||||
[[ -z "${metadata_branch}" ]] && continue
|
||||
metadata_family="$(family_for_branch "${metadata_branch}" || true)"
|
||||
if [[ -z "${metadata_family}" ]]; then
|
||||
fail_resolution "unknown-spec-branch" "spec_branch=${metadata_branch}" "spec_path=${spec_path}"
|
||||
fi
|
||||
if [[ -z "${spec_family}" ]]; then
|
||||
spec_family="${metadata_family}"
|
||||
elif [[ "${spec_family}" != "${metadata_family}" ]]; then
|
||||
fail_resolution "spec-metadata-conflict" "feature_base=${feature_base}" "normal_merge_target=${merge_target}" "spec_path=${spec_path}"
|
||||
fi
|
||||
done
|
||||
|
||||
case "${development_stream}" in
|
||||
platform*) spec_classification="platform" ;;
|
||||
website*) spec_classification="website" ;;
|
||||
cross-stream*) spec_classification="cross-stream" ;;
|
||||
promotion*) spec_classification="promotion" ;;
|
||||
repository-governance*) spec_classification="repository-governance" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ -n "${work_classification}" && -n "${spec_classification}" && "${work_classification}" != "${spec_classification}" ]]; then
|
||||
fail_resolution "work-classification-conflict" "explicit_classification=${work_classification}" "spec_classification=${spec_classification}"
|
||||
fi
|
||||
work_classification="${work_classification:-${spec_classification}}"
|
||||
|
||||
branch_hint=""
|
||||
lower_branch="$(printf '%s' "${branch_name}" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "${lower_branch}" == *platform* && "${lower_branch}" == *website* ]]; then
|
||||
branch_hint="ambiguous"
|
||||
elif [[ "${lower_branch}" == *platform* ]]; then
|
||||
branch_hint="platform"
|
||||
elif [[ "${lower_branch}" == *website* ]]; then
|
||||
branch_hint="website"
|
||||
fi
|
||||
|
||||
scope_hint=""
|
||||
repo_scope="${repo_scope:-${TENANTATLAS_REPO_SCOPE:-}}"
|
||||
case "${repo_scope}" in
|
||||
""|repository)
|
||||
;;
|
||||
platform|website)
|
||||
scope_hint="${repo_scope}"
|
||||
;;
|
||||
*)
|
||||
fail_resolution "invalid-repository-scope" "repo_scope=${repo_scope}"
|
||||
;;
|
||||
esac
|
||||
|
||||
hint_family=""
|
||||
hint_ambiguous=false
|
||||
if [[ "${branch_hint}" == "ambiguous" ]]; then
|
||||
hint_ambiguous=true
|
||||
elif [[ -n "${branch_hint}" && -n "${scope_hint}" ]]; then
|
||||
if [[ "${branch_hint}" != "${scope_hint}" ]]; then
|
||||
hint_ambiguous=true
|
||||
else
|
||||
hint_family="${branch_hint}"
|
||||
fi
|
||||
fi
|
||||
|
||||
selected_family=""
|
||||
family_source=""
|
||||
|
||||
if [[ -n "${explicit_family}" ]]; then
|
||||
selected_family="${explicit_family}"
|
||||
family_source="explicit"
|
||||
fi
|
||||
|
||||
if [[ -n "${target_family}" ]]; then
|
||||
if [[ -n "${selected_family}" && "${selected_family}" != "${target_family}" ]]; then
|
||||
fail_resolution "branch-family-conflict" "explicit=${selected_family}" "pull_request_target=${target_family}"
|
||||
fi
|
||||
if [[ -z "${selected_family}" ]]; then
|
||||
selected_family="${target_family}"
|
||||
family_source="pull-request-target"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${spec_family}" ]]; then
|
||||
if [[ -n "${selected_family}" && "${selected_family}" != "${spec_family}" ]]; then
|
||||
fail_resolution "branch-family-conflict" "selected=${selected_family}" "spec_metadata=${spec_family}" "spec_path=${spec_path}"
|
||||
fi
|
||||
if [[ -z "${selected_family}" ]]; then
|
||||
selected_family="${spec_family}"
|
||||
family_source="spec-metadata"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${hint_family}" ]]; then
|
||||
if [[ -z "${selected_family}" ]]; then
|
||||
selected_family="${hint_family}"
|
||||
family_source="branch-and-scope"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${selected_family}" ]]; then
|
||||
if [[ "${hint_ambiguous}" == true ]]; then
|
||||
fail_resolution "ambiguous-branch-family" "branch_name=${branch_name}" "repo_scope=${repo_scope:-unset}" "spec_path=${spec_path:-unset}"
|
||||
fi
|
||||
fail_resolution "ambiguous-branch-family" "branch_name=${branch_name:-detached}" "repo_scope=${repo_scope:-unset}" "spec_path=${spec_path:-unset}"
|
||||
fi
|
||||
|
||||
if [[ -z "${work_classification}" ]]; then
|
||||
if [[ "${selected_family}" == "repository-wide" ]]; then
|
||||
fail_resolution "explicit-promotion-classification-required" "pull_request_target=${resolved_target:-unset}"
|
||||
fi
|
||||
work_classification="${selected_family}"
|
||||
fi
|
||||
|
||||
case "${work_classification}:${selected_family}" in
|
||||
platform:platform|website:website|promotion:repository-wide|cross-stream:*|repository-governance:*)
|
||||
;;
|
||||
*)
|
||||
fail_resolution "work-classification-family-conflict" "work_classification=${work_classification}" "branch_family=${selected_family}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${selected_family}" == "repository-wide" && "${work_classification}" != "promotion" && "${work_classification}" != "cross-stream" && "${work_classification}" != "repository-governance" ]]; then
|
||||
fail_resolution "explicit-promotion-classification-required" "work_classification=${work_classification}"
|
||||
fi
|
||||
|
||||
if [[ "${work_classification}" == "promotion" || "${work_classification}" == "cross-stream" || "${work_classification}" == "repository-governance" ]]; then
|
||||
if [[ -z "${resolved_target}" && -z "${spec_declared_target}" ]]; then
|
||||
fail_resolution "explicit-target-required" "work_classification=${work_classification}" "branch_family=${selected_family}"
|
||||
fi
|
||||
fi
|
||||
|
||||
expected_branch="$(expected_branch_for_family "${selected_family}")"
|
||||
selected_ref=""
|
||||
|
||||
if [[ -n "${explicit_ref}" ]]; then
|
||||
explicit_ref_family="$(family_for_branch "${explicit_ref}" || true)"
|
||||
if [[ -z "${explicit_ref_family}" || "${explicit_ref_family}" != "${selected_family}" ]]; then
|
||||
fail_resolution "wrong-family-ref" "expected_branch=${expected_branch}" "explicit_ref=${explicit_ref}"
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify "${explicit_ref}^{commit}" >/dev/null 2>&1; then
|
||||
fail_resolution "explicit-ref-unavailable" "expected_branch=${expected_branch}" "explicit_ref=${explicit_ref}"
|
||||
fi
|
||||
|
||||
selected_ref="${explicit_ref}"
|
||||
elif git rev-parse --verify "refs/remotes/origin/${expected_branch}^{commit}" >/dev/null 2>&1; then
|
||||
selected_ref="origin/${expected_branch}"
|
||||
elif git rev-parse --verify "refs/heads/${expected_branch}^{commit}" >/dev/null 2>&1; then
|
||||
selected_ref="${expected_branch}"
|
||||
else
|
||||
fail_resolution "missing-family-ref" "branch_family=${selected_family}" "expected_branch=${expected_branch}" "attempted_refs=origin/${expected_branch},${expected_branch}"
|
||||
fi
|
||||
|
||||
target_commit="$(git rev-parse --verify "${selected_ref}^{commit}" 2>/dev/null || true)"
|
||||
if [[ -z "${target_commit}" ]]; then
|
||||
fail_resolution "selected-ref-unavailable" "selected_ref=${selected_ref}"
|
||||
fi
|
||||
|
||||
merge_base="$(git merge-base HEAD "${selected_ref}" 2>/dev/null || true)"
|
||||
if [[ -z "${merge_base}" ]]; then
|
||||
fail_resolution "no-common-merge-base" "selected_ref=${selected_ref}" "target_commit=${target_commit}"
|
||||
fi
|
||||
|
||||
base_range="${merge_base}...HEAD"
|
||||
|
||||
if [[ "${format}" == "human" ]]; then
|
||||
cat <<EOF
|
||||
Feature base resolved.
|
||||
Branch family: ${selected_family}
|
||||
Work classification: ${work_classification}
|
||||
Family source: ${family_source}
|
||||
Expected integration branch: ${expected_branch}
|
||||
Selected ref: ${selected_ref}
|
||||
Target commit: ${target_commit}
|
||||
Merge base: ${merge_base}
|
||||
Base range: ${base_range}
|
||||
EOF
|
||||
else
|
||||
cat <<EOF
|
||||
status=resolved
|
||||
branch_family=${selected_family}
|
||||
work_classification=${work_classification}
|
||||
family_source=${family_source}
|
||||
expected_branch=${expected_branch}
|
||||
selected_ref=${selected_ref}
|
||||
target_commit=${target_commit}
|
||||
merge_base=${merge_base}
|
||||
base_range=${base_range}
|
||||
EOF
|
||||
fi
|
||||
@ -5,6 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
GUARD_SOURCE="${ROOT_DIR}/scripts/check-ui-productization-coverage"
|
||||
RESOLVER_SOURCE="${ROOT_DIR}/scripts/resolve-feature-base"
|
||||
TEMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/ui-productization-guard.XXXXXX")"
|
||||
FAILED=0
|
||||
|
||||
@ -37,7 +38,8 @@ setup_repo() {
|
||||
|
||||
mkdir -p "${repo}/scripts"
|
||||
cp "${GUARD_SOURCE}" "${repo}/scripts/check-ui-productization-coverage"
|
||||
chmod +x "${repo}/scripts/check-ui-productization-coverage"
|
||||
cp "${RESOLVER_SOURCE}" "${repo}/scripts/resolve-feature-base"
|
||||
chmod +x "${repo}/scripts/check-ui-productization-coverage" "${repo}/scripts/resolve-feature-base"
|
||||
|
||||
write_file "${repo}/apps/platform/app/Filament/Dashboard.php" "<?php\n// baseline\n"
|
||||
write_file "${repo}/apps/platform/app/Support/Navigation/WorkspaceNavigation.php" "<?php\n// baseline\n"
|
||||
@ -49,6 +51,9 @@ setup_repo() {
|
||||
|
||||
git -C "${repo}" add .
|
||||
git -C "${repo}" commit -q -m "baseline"
|
||||
git -C "${repo}" branch platform-dev
|
||||
git -C "${repo}" update-ref refs/remotes/origin/platform-dev HEAD
|
||||
git -C "${repo}" checkout -q -b feat/999-guard-validation
|
||||
|
||||
printf '%s\n' "${repo}"
|
||||
}
|
||||
@ -141,6 +146,21 @@ case_untracked_guarded_ui_without_acknowledgment() {
|
||||
write_file "${repo}/apps/platform/app/Filament/NewUntrackedPage.php" "<?php\n// untracked guarded UI file\n"
|
||||
}
|
||||
|
||||
case_committed_guarded_ui_without_acknowledgment() {
|
||||
local repo="$1"
|
||||
|
||||
touch_filament_page "${repo}"
|
||||
git -C "${repo}" add apps/platform/app/Filament/Dashboard.php
|
||||
git -C "${repo}" commit -q -m "committed guarded UI change"
|
||||
}
|
||||
|
||||
case_staged_guarded_ui_without_acknowledgment() {
|
||||
local repo="$1"
|
||||
|
||||
touch_filament_page "${repo}"
|
||||
git -C "${repo}" add apps/platform/app/Filament/Dashboard.php
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local case_id="$1"
|
||||
local expected="$2"
|
||||
@ -155,12 +175,46 @@ run_case() {
|
||||
set +e
|
||||
(
|
||||
cd "${repo}"
|
||||
bash scripts/check-ui-productization-coverage HEAD
|
||||
env \
|
||||
-u TENANTATLAS_BRANCH_FAMILY \
|
||||
-u TENANTATLAS_WORK_CLASSIFICATION \
|
||||
-u TENANTATLAS_PR_TARGET \
|
||||
-u TENANTATLAS_ACTIVE_SPEC \
|
||||
-u TENANTATLAS_BASE_REF \
|
||||
-u TENANTATLAS_REPO_SCOPE \
|
||||
-u GITHUB_BASE_REF \
|
||||
-u GITEA_BASE_REF \
|
||||
bash scripts/check-ui-productization-coverage platform-dev --branch-family=platform
|
||||
) >"${output_file}" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ "${expected}" == "pass" && ${status} -eq 0 ]] || [[ "${expected}" == "fail" && ${status} -ne 0 ]]; then
|
||||
local expected_status=0
|
||||
[[ "${expected}" == "coverage-fail" ]] && expected_status=1
|
||||
[[ "${expected}" == "resolution-fail" ]] && expected_status=2
|
||||
|
||||
if [[ ${status} -eq ${expected_status} ]]; then
|
||||
if [[ "${expected}" != "resolution-fail" ]] && ! grep -q '^branch_family=platform$' "${output_file}"; then
|
||||
FAILED=1
|
||||
printf 'FAIL %s did not print resolver family diagnostics\n' "${case_id}" >&2
|
||||
sed 's/^/ /' "${output_file}" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${expected}" != "resolution-fail" ]] && ! grep -q '^selected_ref=platform-dev$' "${output_file}"; then
|
||||
FAILED=1
|
||||
printf 'FAIL %s did not preserve the explicit family-matching ref\n' "${case_id}" >&2
|
||||
sed 's/^/ /' "${output_file}" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${expected}" != "resolution-fail" ]] && ! grep -q '^base_range=[0-9a-f]\{40\}\.\.\.HEAD$' "${output_file}"; then
|
||||
FAILED=1
|
||||
printf 'FAIL %s did not print a merge-base-derived range\n' "${case_id}" >&2
|
||||
sed 's/^/ /' "${output_file}" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
printf 'PASS %s (%s)\n' "${case_id}" "${expected}"
|
||||
return
|
||||
fi
|
||||
@ -170,17 +224,69 @@ run_case() {
|
||||
sed 's/^/ /' "${output_file}" >&2
|
||||
}
|
||||
|
||||
run_case "ui_unchecked_template_heading_fails" "fail" "case_ui_unchecked_template_heading"
|
||||
run_case "ui_unchecked_spec_checkbox_fails" "fail" "case_ui_unchecked_spec_checkbox"
|
||||
run_resolution_failure_before_diff_case() {
|
||||
local case_id="resolver_failure_stops_before_diff"
|
||||
local repo
|
||||
local status
|
||||
local output_file="${TEMP_ROOT}/${case_id}.out"
|
||||
local shim_directory="${TEMP_ROOT}/${case_id}-bin"
|
||||
local diff_marker="${TEMP_ROOT}/${case_id}.diff-ran"
|
||||
local real_git
|
||||
|
||||
repo="$(setup_repo "${case_id}")"
|
||||
real_git="$(command -v git)"
|
||||
mkdir -p "${shim_directory}"
|
||||
write_file "${shim_directory}/git" '#!/usr/bin/env bash\nfor argument in "$@"; do\n if [[ "${argument}" == "diff" ]]; then\n : > "${DIFF_MARKER}"\n fi\ndone\nexec "${REAL_GIT}" "$@"\n'
|
||||
chmod +x "${shim_directory}/git"
|
||||
|
||||
set +e
|
||||
(
|
||||
cd "${repo}"
|
||||
export PATH="${shim_directory}:${PATH}"
|
||||
export REAL_GIT="${real_git}"
|
||||
export DIFF_MARKER="${diff_marker}"
|
||||
env \
|
||||
-u TENANTATLAS_BRANCH_FAMILY \
|
||||
-u TENANTATLAS_WORK_CLASSIFICATION \
|
||||
-u TENANTATLAS_PR_TARGET \
|
||||
-u TENANTATLAS_ACTIVE_SPEC \
|
||||
-u TENANTATLAS_BASE_REF \
|
||||
-u TENANTATLAS_REPO_SCOPE \
|
||||
-u GITHUB_BASE_REF \
|
||||
-u GITEA_BASE_REF \
|
||||
bash scripts/check-ui-productization-coverage origin/website-dev --branch-family=platform
|
||||
) >"${output_file}" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ ${status} -eq 2 ]] \
|
||||
&& [[ ! -e "${diff_marker}" ]] \
|
||||
&& grep -q '^reason=wrong-family-ref$' "${output_file}" \
|
||||
&& grep -q '^diff_ran=false$' "${output_file}"; then
|
||||
printf 'PASS %s (resolution-fail)\n' "${case_id}"
|
||||
return
|
||||
fi
|
||||
|
||||
FAILED=1
|
||||
printf 'FAIL %s expected resolver exit 2 before any git diff\n' "${case_id}" >&2
|
||||
sed 's/^/ /' "${output_file}" >&2
|
||||
}
|
||||
|
||||
run_case "ui_unchecked_template_heading_fails" "coverage-fail" "case_ui_unchecked_template_heading"
|
||||
run_case "ui_unchecked_spec_checkbox_fails" "coverage-fail" "case_ui_unchecked_spec_checkbox"
|
||||
run_case "ui_checked_impact_spec_passes" "pass" "case_ui_checked_impact_spec"
|
||||
run_case "ui_checked_no_impact_with_rationale_passes" "pass" "case_ui_checked_no_impact_with_rationale"
|
||||
run_case "ui_checked_no_impact_without_rationale_fails" "fail" "case_ui_checked_no_impact_without_rationale"
|
||||
run_case "ui_checked_no_impact_without_rationale_fails" "coverage-fail" "case_ui_checked_no_impact_without_rationale"
|
||||
run_case "ui_real_audit_coverage_artifact_passes" "pass" "case_ui_real_audit_coverage_artifact"
|
||||
run_case "navigation_provider_without_acknowledgment_fails" "fail" "case_navigation_provider_without_acknowledgment"
|
||||
run_case "navigation_provider_without_acknowledgment_fails" "coverage-fail" "case_navigation_provider_without_acknowledgment"
|
||||
run_case "navigation_provider_with_checked_acknowledgment_passes" "pass" "case_navigation_provider_with_checked_acknowledgment"
|
||||
run_case "backend_only_diff_passes" "pass" "case_backend_only_diff"
|
||||
run_case "docs_only_diff_passes" "pass" "case_docs_only_diff"
|
||||
run_case "untracked_guarded_ui_without_acknowledgment_fails" "fail" "case_untracked_guarded_ui_without_acknowledgment"
|
||||
run_case "committed_guarded_ui_without_acknowledgment_fails" "coverage-fail" "case_committed_guarded_ui_without_acknowledgment"
|
||||
run_case "staged_guarded_ui_without_acknowledgment_fails" "coverage-fail" "case_staged_guarded_ui_without_acknowledgment"
|
||||
run_case "unstaged_guarded_ui_without_acknowledgment_fails" "coverage-fail" "case_ui_unchecked_template_heading"
|
||||
run_case "untracked_guarded_ui_without_acknowledgment_fails" "coverage-fail" "case_untracked_guarded_ui_without_acknowledgment"
|
||||
run_resolution_failure_before_diff_case
|
||||
|
||||
if [[ ${FAILED} -ne 0 ]]; then
|
||||
exit 1
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
# Requirements Quality Checklist: Spec 439 - Branch Topology and Local Evidence Truth
|
||||
|
||||
**Purpose**: Validate that the corrected Spec 439 requirements are complete, unambiguous, repo-grounded, proportional, internally consistent, and ready for a separate implementation loop. This checklist tests requirements quality, not implementation.
|
||||
**Created**: 2026-07-10
|
||||
**Corrected**: 2026-07-11
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
**Snapshot boundary**: This checklist records the requirements-preparation close-out before implementation began. Current implementation/task/evidence state is owned by [tasks.md](../tasks.md) and, after Phase 8, [implementation-report.md](../implementation-report.md); the historical statements below are not live implementation status.
|
||||
|
||||
## Candidate and Repository Truth
|
||||
|
||||
- [x] CHK001 Is the candidate identified as directly user-provided and bounded to current repository-governance/evidence defects? [Completeness, Spec §Preparation Selection Summary]
|
||||
- [x] CHK002 Are the wrong-base, false-report-success, JUnit-validity, and blocking-budget defects named at current repo call paths without claiming implementation? [Traceability, Spec §Context and Problem Statement; Plan §Correction Preflight Evidence]
|
||||
- [x] CHK003 Are Spec 437 and Spec 438 classified as read-only context with an explicit unchanged requirement? [Consistency, Spec §Preparation Selection Summary; FR-052]
|
||||
- [x] CHK004 Does the Candidate Check contain the mandatory problem, failure, improvement, smallest version, complexity, why-now, why-not-local, class, red flags, defense, score, and decision fields? [Completeness, Spec §Spec Candidate Check]
|
||||
- [x] CHK005 Is the 11/12 score arithmetically correct and supported by current workflow/evidence value rather than future flexibility? [Measurability, Spec §Spec Candidate Check]
|
||||
- [x] CHK006 Are candidate approval, requirements readiness, and implementation completion kept as three separate claims? [Clarity, Spec §Candidate and Readiness Decision; §Final Spec Gate]
|
||||
|
||||
## Constitution Amendment Authorization and Scope
|
||||
|
||||
- [x] CHK007 Does the spec explicitly authorize the narrow Constitution branch-topology amendment as its first mutating implementation step? [Completeness, Spec §Constitution Amendment Authorization; FR-001]
|
||||
- [x] CHK008 Is the governance basis documented: explicit Constitution update PR, rationale, impacted artifacts, SemVer decision, and Last Amended, with no mandatory separate preceding feature spec? [Traceability, Spec §Governance basis; FR-002, FR-007]
|
||||
- [x] CHK009 Does the amendment scope define exactly the three branch roles, scope-specific routing, explicit cross-stream/promotion classification, and no wrong-family/HEAD~1 fallback? [Completeness, Spec §Authorized amendment scope; FR-003]
|
||||
- [x] CHK010 Are general Constitution reduction, UI/UX cleanup, skill reorganization, architecture/security cleanup, and unrelated principles explicitly excluded? [Scope, Spec §Forbidden amendment scope; FR-004]
|
||||
- [x] CHK011 Does the spec distinguish authorization of the future amendment from falsely claiming that the Constitution is already amended? [Clarity, Spec §Governance basis]
|
||||
- [x] CHK012 Is SemVer left for an explicit policy-based Phase 1 decision, with PATCH not silently predetermined? [Clarity, Plan §SemVer handling; FR-002]
|
||||
- [x] CHK013 Are the required amendment metadata and unchanged Ratified-date expectation measurable in Phase 1 evidence? [Measurability, Plan §Phase 1; Tasks T003-T006]
|
||||
- [x] CHK014 Is the same-feature-PR posture clear without weakening the rule that dependent work waits for amendment validation? [Consistency, Spec §Candidate and Readiness Decision; FR-005, FR-007]
|
||||
|
||||
## Mandatory Phase and Gate Quality
|
||||
|
||||
- [x] CHK015 Are Phases 0 through 8 named in the required order in both spec and plan? [Consistency, Spec §Mandatory Implementation Phases; Plan §Mandatory Implementation Phases]
|
||||
- [x] CHK016 Is Phase 0 explicitly read-only and the only work permitted before the first mutation? [Clarity, Spec §Mandatory gate sequence; Plan §Phase 0]
|
||||
- [x] CHK017 Is Phase 1 the narrow Constitution amendment rather than an external prerequisite? [Consistency, Spec §Phase 1; Plan §Phase 1; Tasks Phase 1]
|
||||
- [x] CHK018 Does T006 form a global hard gate that blocks every Phase 2-8 task? [Measurability, Tasks §Phase 1 Hard Gate; FR-005, INV-002]
|
||||
- [x] CHK019 Are direct instruction synchronization, resolver, guards/workflow declarations, evidence truth, budget truth, proof, and report strictly ordered after Phase 1? [Consistency, Spec/Plan §Mandatory Implementation Phases]
|
||||
- [x] CHK020 Are stop conditions strong enough to halt all dependent work when amendment validation fails? [Exception Coverage, Spec §Stop Conditions]
|
||||
- [x] CHK021 Does the parallelization contract forbid phase-gate bypass and same-file concurrent ownership? [Clarity, Spec/Plan §Parallelization Contract; Tasks §Parallel Opportunities]
|
||||
|
||||
## Branch Topology Requirement Quality
|
||||
|
||||
- [x] CHK022 Is dev defined only as repository-wide integration/promotion rather than an ordinary stream default? [Clarity, Spec §Canonical branch roles; FR-008]
|
||||
- [x] CHK023 Is platform-dev consistently the platform feature base, normal PR target, and diff baseline? [Consistency, Spec §Normal feature routing; Plan §Canonical Branch Topology; FR-009]
|
||||
- [x] CHK024 Is website-dev consistently the website feature base, normal PR target, and diff baseline? [Consistency, Spec §Normal feature routing; Plan §Canonical Branch Topology; FR-010]
|
||||
- [x] CHK025 Is cross-stream/promotion work required to declare classification, target, and baseline and allowed to target dev only explicitly? [Clarity, FR-011, FR-012]
|
||||
- [x] CHK026 Is repository-governance work distinguished from a fourth branch family and explicitly mapped for Spec 439 by affected scope? [Clarity, Spec §Branch and Integration Strategy]
|
||||
- [x] CHK027 Does the spec state that long-lived branch divergence is not itself a defect? [Ambiguity Prevention, FR-013]
|
||||
- [x] CHK028 Is website resolver proof distinguished from actual delivery onto website-dev? [Scope, Spec §Branch and Integration Strategy]
|
||||
|
||||
## Unified Resolver Contract Quality
|
||||
|
||||
- [x] CHK029 Are branch-family selection and concrete Git-ref selection defined as two separate decisions? [Clarity, Spec/Plan §Unified Resolver Contract; INV-010]
|
||||
- [x] CHK030 Is Stage 1 priority exactly explicit family input, PR target, active spec metadata/classification, branch-name plus repository-scope signals, then fail closed? [Consistency, FR-014; Spec/Plan §Stage 1]
|
||||
- [x] CHK031 Is every explicit family input required to be validated against a known family? [Completeness, FR-015]
|
||||
- [x] CHK032 Is a known PR target authoritative while still being conflict-checked against other authoritative signals? [Clarity, FR-016]
|
||||
- [x] CHK033 Are spec metadata, branch naming, and path/scope signals prevented from silently overriding a conflicting valid PR target? [Conflict, FR-017-FR-019]
|
||||
- [x] CHK034 Is explicit family platform plus PR target website-dev defined as an actionable failure rather than precedence-based override? [Conflict, Spec §Stage 1; BS-010]
|
||||
- [x] CHK035 Is branch-name-only or ambiguous heuristic classification forbidden? [Ambiguity, FR-018-FR-020; BS-011]
|
||||
- [x] CHK036 Is Stage 2 priority exactly explicit family-matching ref, matching remote ref, matching local ref, ancestry/merge-base validation, then fail closed? [Consistency, FR-021, FR-022]
|
||||
- [x] CHK037 Are refs from a different family rejected before merge-base/diff execution? [Exception Coverage, FR-023; BS-012]
|
||||
- [x] CHK038 Are origin/dev and HEAD~1 explicitly forbidden as platform/website integration fallbacks? [Clarity, FR-024]
|
||||
- [x] CHK039 Is dev permitted in Stage 2 only after Stage 1 explicitly classifies promotion/cross-stream integration? [Consistency, FR-025]
|
||||
- [x] CHK040 Are success and failure diagnostics objectively specified, including family, ref, target, merge base, base range, conflicts/attempted refs, override guidance, and no-diff confirmation? [Measurability, FR-026, FR-027]
|
||||
- [x] CHK041 Do spec and plan use the same resolver priorities, conflict rules, terminology, and examples without an extra ancestry source in Stage 1? [Cross-Artifact Consistency, Spec/Plan §Unified Resolver Contract]
|
||||
|
||||
## Evidence and Budget Truth Requirement Quality
|
||||
|
||||
- [x] CHK042 Is immediate capture and persistence of the original test-process exit required? [Completeness, FR-034]
|
||||
- [x] CHK043 Is report refresh prohibited from hard-coding or inferring success over stored failure truth? [Clarity, FR-035-FR-037]
|
||||
- [x] CHK044 Is dual test/report failure required to preserve both contexts and remain failed? [Exception Coverage, FR-038; EF-007]
|
||||
- [x] CHK045 Are JUnit existence, parse validity, counts, failures, errors, skips, status, and affected path specified? [Completeness, FR-039-FR-041]
|
||||
- [x] CHK046 Are process/JUnit/report contradictions required to be visible and non-zero? [Consistency, FR-040; EF-005]
|
||||
- [x] CHK047 Can missing or malformed required JUnit/report evidence never produce success? [Edge Case, FR-041; EF-006]
|
||||
- [x] CHK048 Are existing valid successful consumers protected through a minimal compatible report extension? [Compatibility, FR-042]
|
||||
- [x] CHK049 Is TestLaneBudget the sole blocking/advisory authority, with no policy duplication in wrappers/Composer/workflow declarations? [Ownership, FR-043, FR-044]
|
||||
- [x] CHK050 Are blocking, advisory, threshold-preservation, missing-input, and reusable-entry-point outcomes objectively distinct? [Measurability, FR-045-FR-049; EF-003, EF-004]
|
||||
|
||||
## Scope, Product Surface, and Proportionality Quality
|
||||
|
||||
- [x] CHK051 Is the implementation bounded to governance docs, repository tooling, local evidence/report support, focused tests, and a final report? [Scope, Spec §In Scope]
|
||||
- [x] CHK052 Are remote branch protection, Gitea administration, deployment gates, remote merge governance, and CI-platform redesign excluded? [Scope, FR-053; Spec §Explicit Non-Goals]
|
||||
- [x] CHK053 Are product runtime, UI, routes, models, migrations, providers, RBAC, OperationRun, customer output, and website runtime explicitly excluded? [Scope, FR-050]
|
||||
- [x] CHK054 Are deterministic, network-free, database-free fixtures required without hidden broad context cost? [Non-Functional Coverage, FR-051; Spec §Testing / Lane / Runtime Impact]
|
||||
- [x] CHK055 Does the proportionality review justify one fixed two-stage resolver and one narrow governance amendment while rejecting a general framework? [Proportionality, Spec §Proportionality Review]
|
||||
- [x] CHK056 Are Product Surface, browser proof, Human Product Sanity, assets, and deployment consistently N/A with rationale? [Consistency, Spec/Plan Product and Runtime sections]
|
||||
|
||||
## Task and Implementation-Evidence Quality
|
||||
|
||||
- [x] CHK057 Do all numbered tasks include phase, requirement/invariant references, dependencies, exact file/component, and expected evidence? [Task Quality, Tasks T001-T061]
|
||||
- [x] CHK058 Were all tasks still unchecked at requirements-preparation close-out, with Spec-Correction clearly distinguished from implementation? [Historical State Clarity, Tasks preparation snapshot]
|
||||
- [x] CHK059 Does the coverage table map FR-001-FR-054, INV-001-INV-020, BS-001-BS-012, and EF-001-EF-007 to tasks? [Traceability, Tasks §Requirement and Evidence Coverage]
|
||||
- [x] CHK060 Was implementation-report.md reserved for Phase 8 and explicitly absent during requirements preparation? [Historical State Clarity, Tasks Phase 8; Spec §Final Spec Gate]
|
||||
|
||||
## Post-Implementation Evidence-Contract Correction Addendum
|
||||
|
||||
This addendum records the current evidence-contract and baseline-attribution
|
||||
review. It does not rewrite the CHK001-CHK060 requirements-preparation
|
||||
snapshot.
|
||||
|
||||
- [x] CHK061 Does the corrected contract preserve the prospective Phase 1 hard gate while defining proportional close-out evidence?
|
||||
- [x] CHK062 Are the final Constitution amendment, exact amendment boundary, unrelated-scope controls, synchronized dependents, and absence of a conflicting final state repo-proven?
|
||||
- [x] CHK063 Do `tasks.md` and `implementation-report.md` attest the sequence and state explicitly that no pre-mutation historical checkpoint was required or retroactively claimed?
|
||||
- [x] CHK064 Does the paired baseline attribution use the exact pre-439 SHA, comparable environment and lane selection, raw exits, JUnit documents, and separate artifact checksums?
|
||||
- [x] CHK065 Are failure/error cases compared as multisets and performance, baseline debt, feature regression, and gate activation kept distinct without changing budgets?
|
||||
- [x] CHK066 Do T056, T061, the attribution evidence, and the implementation report agree, without describing a failed baseline or feature lane as PASS?
|
||||
|
||||
## Requirements Quality Result
|
||||
|
||||
- **Candidate Gate**: PASS.
|
||||
- **Requirements quality**: PASS.
|
||||
- **Cross-artifact analysis**: PASS after corrected spec/plan/tasks/checklist validation.
|
||||
- **Spec Readiness Gate**: READY.
|
||||
- **Implementation Authorization**: AUTHORIZED.
|
||||
- **Constitution-Amendment Posture at preparation close-out**: IN SCOPE AS MANDATORY PHASE 1; NOT YET IMPLEMENTED.
|
||||
- **Requirements quality complete**: yes.
|
||||
- **Implementation performed at preparation close-out**: no.
|
||||
- **Implementation complete at preparation close-out**: no - NOT STARTED.
|
||||
- **Implementation report created at preparation close-out**: no.
|
||||
- **Test execution claimed by this requirements snapshot**: no.
|
||||
- **Requirements review outcome class**: acceptable-special-case.
|
||||
- **Test-governance preparation outcome**: document-in-feature.
|
||||
- **Next action recorded at preparation close-out**: Start a separate Spec 439 implementation loop at Phase 0, then execute and validate Phase 1 before any dependent task.
|
||||
@ -0,0 +1,484 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"recordedAt": "2026-07-11",
|
||||
"purpose": "Spec 439 paired clean-baseline attribution and final evidence reconciliation",
|
||||
"baseline": {
|
||||
"ref": "origin/platform-dev",
|
||||
"sha": "71e3745aee7e69dbe2479ef622075d73c36b717a",
|
||||
"localPlatformDevSha": "71e3745aee7e69dbe2479ef622075d73c36b717a",
|
||||
"mergeBaseSha": "71e3745aee7e69dbe2479ef622075d73c36b717a",
|
||||
"worktreePath": "/Users/ahmeddarrazi/Documents/projects/wt-plattform-spec439-baseline-71e3745a",
|
||||
"detached": true,
|
||||
"cleanDuringExecution": true,
|
||||
"removedAfterEvidenceCapture": true,
|
||||
"temporaryComposeProjectRemoved": true,
|
||||
"fetchPerformed": false
|
||||
},
|
||||
"feature": {
|
||||
"branch": "feat/439-branch-topology-local-evidence-truth",
|
||||
"head": "71e3745aee7e69dbe2479ef622075d73c36b717a",
|
||||
"testedWorktreeDigest": "a0755a5c650cb2704a1d2d47d8c4f616526b391da9adc6c91f4c3f3818b713a5",
|
||||
"testedWorktreeDigestAlgorithm": "SHA-256 over sorted 26-path records path\\0file-sha256\\n before evidence-contract documentation reconciliation",
|
||||
"trackedModifiedPathCount": 18,
|
||||
"untrackedFileCountBeforeEvidence": 8
|
||||
},
|
||||
"commands": {
|
||||
"nativeFastFeedback": "./scripts/platform-test-lane fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request",
|
||||
"nativeConfidence": "./scripts/platform-test-lane confidence --workflow-id=main-confidence --trigger-class=mainline-push",
|
||||
"normalizedRunner": "php vendor/bin/pest --colors=never --parallel --configuration=<generated-identical-selection-config> --testsuite=Lane --log-junit=<separate-path> --profile",
|
||||
"browserPreconditionCorrection": "php vendor/bin/pest --colors=never --configuration=phpunit.xml --filter='it rejects browser placement in non-browser lanes and keeps the default loops clean' --log-junit=<separate-path> tests/Feature/Guards/BrowserLaneIsolationTest.php"
|
||||
},
|
||||
"environment": {
|
||||
"equivalent": true,
|
||||
"php": "8.4.15",
|
||||
"laravel": "12.52.0",
|
||||
"pest": "4.3.1",
|
||||
"phpunit": "12.5.4",
|
||||
"postgresqlServer": "16.11",
|
||||
"testDatabase": "SQLite in-memory",
|
||||
"architecture": "arm64",
|
||||
"cpuCount": 8,
|
||||
"hostTimezone": "Europe/Berlin",
|
||||
"containerTimezone": "UTC",
|
||||
"sailContext": "separate Compose projects and containers; identical application image",
|
||||
"featureComposeProject": "wt-plattform",
|
||||
"baselineComposeProject": "spec439-baseline",
|
||||
"containerImage": "sha256:a2822bcc087a2282276ca8e38590ec166dc0c73c40e7a44362cfe94b98d06f5b",
|
||||
"hashes": {
|
||||
"composerLock": "ae033b4c25a9c1d57cecc1329a0835562f02ee728f3ca97f4c57057c5b9c8991",
|
||||
"dockerCompose": "336ef101f60d94dbf1ddde80e9d24555fefcbf053cf5f5bd43252e2e1997c1f7",
|
||||
"phpunitXml": "db1a11bbbce7c77ff7a78bd8838baeced86c24fa10ca15396c5a888141320756",
|
||||
"environmentFile": "9b93ebb2ee7ff445998704b313dfe841a09e61de1b84fe32b3619d06fbb0bb57"
|
||||
}
|
||||
},
|
||||
"budgetContract": {
|
||||
"unchanged": true,
|
||||
"methodSourceSha256": "6e1d54496ff36d8d8bc8596e4520261f65917d2615e8f640d2d363b6e23145dc",
|
||||
"relevantProfilesSha256": "7b0d5a54ae0b7c406ff85fe95a614583b0e4221d1e43ca3ab7a0fdb8248d91ef",
|
||||
"profiles": {
|
||||
"fastFeedbackPullRequest": {
|
||||
"baseSeconds": 200,
|
||||
"varianceSeconds": 15,
|
||||
"effectiveSeconds": 215,
|
||||
"mode": "hard-fail"
|
||||
},
|
||||
"confidenceMainlinePush": {
|
||||
"baseSeconds": 450,
|
||||
"varianceSeconds": 30,
|
||||
"effectiveSeconds": 480,
|
||||
"mode": "soft-warn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nativeSelection": {
|
||||
"comparableForDirectCounts": false,
|
||||
"reason": "Spec 439 adds two contract files and changes test identities; identical-selection supplemental runs are authoritative for functional attribution",
|
||||
"fastFeedback": {
|
||||
"baselineFileCount": 679,
|
||||
"featureFileCount": 681,
|
||||
"baselineFileManifestSha256": "2dde6bae2fdfa471730bc8e90f7a0a0132ff658d81a4f69b0148476040c592ad",
|
||||
"featureFileManifestSha256": "0d74b73ffa69dd529ebd41f000875279a25504200fbfae1e108a23919cc42252",
|
||||
"baselineIdentitySha256": "267f48fb0460d28a55578423ed48e7c368716ec428886b8a76a85fc23f7fa212",
|
||||
"featureIdentitySha256": "0374b3eaa57a417530e47632ac0b76cfd03c73c81fa978e8d806f955c9fe6e71"
|
||||
},
|
||||
"confidence": {
|
||||
"baselineFileCount": 1594,
|
||||
"featureFileCount": 1596,
|
||||
"baselineFileManifestSha256": "e2ca4ce14a2c620d5adeebeb72c3fcfb38f879926b705686dace31ef461f436e",
|
||||
"featureFileManifestSha256": "5ae5d9745d9cb62124b180e9bc3a613bf0b5e7d0a96e30ae1941797c0de0937d",
|
||||
"baselineIdentitySha256": "ebeb2c3ef35d0ab3d35a01ab50c1048e4ee26849b0dd15a75c0ee969e9d2852a",
|
||||
"featureIdentitySha256": "a72332226a18e278034df2507dfb4c2c44f6d74a746e988d8ed814e047ee7419"
|
||||
},
|
||||
"identityDelta": {
|
||||
"baselineOnly": 2,
|
||||
"featureOnly": 42,
|
||||
"netFeatureTests": 40,
|
||||
"allFeatureOnlyIdentitiesPassed": true
|
||||
}
|
||||
},
|
||||
"nativeRuns": {
|
||||
"fastFeedback": {
|
||||
"workflowId": "pr-fast-feedback",
|
||||
"triggerClass": "pull-request",
|
||||
"baseline": {
|
||||
"shellExit": 2,
|
||||
"tests": 2825,
|
||||
"assertions": 17404,
|
||||
"failures": 39,
|
||||
"errors": 5,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": 1384.556496,
|
||||
"laneWallSeconds": 187.344327,
|
||||
"normalizedCiBudgetStatus": "within-budget",
|
||||
"reportStatus": "failed",
|
||||
"artifactStatus": "complete"
|
||||
},
|
||||
"feature": {
|
||||
"shellExit": 2,
|
||||
"tests": 2865,
|
||||
"assertions": 20511,
|
||||
"failures": 37,
|
||||
"errors": 3,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": 1418.066647,
|
||||
"laneWallSeconds": 191.234712,
|
||||
"budgetStatus": "within-budget",
|
||||
"reportStatus": "failed",
|
||||
"artifactStatus": "complete"
|
||||
},
|
||||
"deltaFeatureMinusBaseline": {
|
||||
"tests": 40,
|
||||
"failures": -2,
|
||||
"errors": -2,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": 33.510151,
|
||||
"laneWallSeconds": 3.890385,
|
||||
"laneWallPercent": 2.076596,
|
||||
"shellExitChanged": false,
|
||||
"reportClassificationChanged": false,
|
||||
"budgetClassificationChanged": false
|
||||
}
|
||||
},
|
||||
"confidence": {
|
||||
"workflowId": "main-confidence",
|
||||
"triggerClass": "mainline-push",
|
||||
"baseline": {
|
||||
"shellExit": 2,
|
||||
"tests": 6144,
|
||||
"assertions": 39731,
|
||||
"failures": 97,
|
||||
"errors": 26,
|
||||
"skips": 26,
|
||||
"junitCumulativeSeconds": 4545.602722,
|
||||
"laneWallSeconds": 614.802711,
|
||||
"normalizedCiBudgetStatus": "over-budget",
|
||||
"normalizedCiBlockingStatus": "non-blocking-warning",
|
||||
"reportStatus": "failed",
|
||||
"artifactStatus": "complete"
|
||||
},
|
||||
"feature": {
|
||||
"shellExit": 2,
|
||||
"tests": 6184,
|
||||
"assertions": 42837,
|
||||
"failures": 95,
|
||||
"errors": 23,
|
||||
"skips": 26,
|
||||
"junitCumulativeSeconds": 4357.936975,
|
||||
"laneWallSeconds": 590.987661,
|
||||
"budgetStatus": "over-budget",
|
||||
"budgetBlockingStatus": "non-blocking-warning",
|
||||
"reportStatus": "failed",
|
||||
"artifactStatus": "complete"
|
||||
},
|
||||
"deltaFeatureMinusBaseline": {
|
||||
"tests": 40,
|
||||
"failures": -2,
|
||||
"errors": -3,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": -187.665747,
|
||||
"laneWallSeconds": -23.81505,
|
||||
"laneWallPercent": -3.873608,
|
||||
"shellExitChanged": false,
|
||||
"reportClassificationChanged": false,
|
||||
"budgetClassificationChanged": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"failureSignatureContract": {
|
||||
"identityFields": [
|
||||
"testsuite",
|
||||
"classname",
|
||||
"testcase"
|
||||
],
|
||||
"outcomeFields": [
|
||||
"status",
|
||||
"type",
|
||||
"normalizedFirstMaterialMessage"
|
||||
],
|
||||
"comparison": "multiset",
|
||||
"normalization": "Paths, generated HTML/CSRF/Livewire values, factory identifiers, and Mockery ordinals are normalized only after semantic status/type/assertion comparison"
|
||||
},
|
||||
"signatureCatalog": {
|
||||
"browserConfigMissing": {
|
||||
"testsuite": "Tests\\Feature\\Guards\\BrowserLaneIsolationTest",
|
||||
"classname": "Tests.Feature.Guards.BrowserLaneIsolationTest",
|
||||
"testcase": "it rejects browser placement in non-browser lanes and keeps the default loops clean",
|
||||
"status": "error",
|
||||
"type": "ErrorException",
|
||||
"normalizedFirstMaterialMessage": "file_get_contents(<app>/storage/logs/test-lanes/browser-lane.phpunit.xml): Failed to open stream: No such file or directory"
|
||||
},
|
||||
"artifactSkeleton": {
|
||||
"testsuite": "Tests\\Feature\\Guards\\TestLaneArtifactsContractTest",
|
||||
"classname": "Tests.Feature.Guards.TestLaneArtifactsContractTest",
|
||||
"testcase": "it keeps only the skeleton file checked into the lane artifact directory",
|
||||
"status": "failure",
|
||||
"type": "PHPUnit\\Framework\\ExpectationFailedException",
|
||||
"normalizedFirstMaterialMessage": "Failed asserting that false is true."
|
||||
},
|
||||
"commandBrowserConfig": {
|
||||
"testsuite": "Tests\\Feature\\Guards\\TestLaneCommandContractTest",
|
||||
"classname": "Tests.Feature.Guards.TestLaneCommandContractTest",
|
||||
"testcase": "it routes the foundational lane commands through stable artisan arguments",
|
||||
"status": "error",
|
||||
"type": "ErrorException",
|
||||
"normalizedFirstMaterialMessage": "file_get_contents(<app>/storage/logs/test-lanes/browser-lane.phpunit.xml): Failed to open stream: No such file or directory"
|
||||
},
|
||||
"recalibrationEvidence": {
|
||||
"testsuite": "Tests\\Feature\\Guards\\TestLaneRecalibrationEvidenceContractTest",
|
||||
"classname": "Tests.Feature.Guards.TestLaneRecalibrationEvidenceContractTest",
|
||||
"testcase": "it emits candidate, approved, and rejected recalibration records with explicit summary disclosure",
|
||||
"status": "failure",
|
||||
"type": "PHPUnit\\Framework\\ExpectationFailedException",
|
||||
"normalizedFirstMaterialMessage": "Failed asserting that two strings are identical."
|
||||
},
|
||||
"livewireManifest": {
|
||||
"testsuite": "Tests\\Feature\\LivewireInterceptShimTest",
|
||||
"classname": "Tests.Feature.LivewireInterceptShimTest",
|
||||
"testcase": "it resolves the built Filament admin theme from the manifest",
|
||||
"status": "error",
|
||||
"type": "Pest\\Exceptions\\InvalidExpectationValue",
|
||||
"normalizedFirstMaterialMessage": "Invalid expectation value type. Expected [iterable]."
|
||||
},
|
||||
"platformRelocation": {
|
||||
"testsuite": "Tests\\Feature\\PlatformRelocation\\RuntimeSmokeTest",
|
||||
"classname": "Tests.Feature.PlatformRelocation.RuntimeSmokeTest",
|
||||
"testcase": "it keeps the relocated public and storage paths aligned with the app root",
|
||||
"status": "failure",
|
||||
"type": "PHPUnit\\Framework\\ExpectationFailedException",
|
||||
"normalizedFirstMaterialMessage": "Failed asserting that false is true."
|
||||
}
|
||||
},
|
||||
"exactFailureAttribution": {
|
||||
"nativeFastFeedback": {
|
||||
"baselineOnlySignatureIds": [
|
||||
"browserConfigMissing",
|
||||
"artifactSkeleton",
|
||||
"commandBrowserConfig",
|
||||
"recalibrationEvidence"
|
||||
],
|
||||
"featureOnly": [],
|
||||
"identicalClassificationCount": 40,
|
||||
"classificationChanged": [],
|
||||
"materialMessageChanged": []
|
||||
},
|
||||
"nativeConfidence": {
|
||||
"baselineOnlySignatureIds": [
|
||||
"browserConfigMissing",
|
||||
"artifactSkeleton",
|
||||
"commandBrowserConfig",
|
||||
"livewireManifest",
|
||||
"platformRelocation"
|
||||
],
|
||||
"featureOnly": [],
|
||||
"identicalClassificationCount": 118,
|
||||
"classificationChanged": [],
|
||||
"materialMessageChanged": []
|
||||
}
|
||||
},
|
||||
"identicalSelectionAttribution": {
|
||||
"excludedChangedTestFiles": [
|
||||
"apps/platform/tests/Feature/Guards/BranchTopologyContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/CiLaneFailureClassificationContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php",
|
||||
"apps/platform/tests/Feature/Guards/TestLaneTrendClassificationTest.php",
|
||||
"apps/platform/tests/Unit/Support/TestLaneBudgetTest.php",
|
||||
"apps/platform/tests/Unit/Support/TestLaneReportTest.php"
|
||||
],
|
||||
"fastFeedback": {
|
||||
"selectedFileCount": 672,
|
||||
"selectionManifestSha256": "c8d47eb76141fe3fae0ed303d874bfe3fd2b6ec6da2424be723d3116b06a933b",
|
||||
"normalizedFileListSha256": "8fba23223b9b6be86db6dfeda70423d07580e7d790d23807da787feb85bbc90c",
|
||||
"identityCount": 2801,
|
||||
"identitySha256": "2d4afdaacdb2ee0caaec5df6ee66b512f8bd7a9bc80ceb7672886b320f1dee94",
|
||||
"rawBaseline": {
|
||||
"shellExit": 2,
|
||||
"tests": 2801,
|
||||
"assertions": 17221,
|
||||
"failures": 37,
|
||||
"errors": 4,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": 1286.489596,
|
||||
"pestSeconds": 168.04,
|
||||
"externalWallSeconds": 175.67
|
||||
},
|
||||
"rawFeature": {
|
||||
"shellExit": 2,
|
||||
"tests": 2801,
|
||||
"assertions": 19504,
|
||||
"failures": 37,
|
||||
"errors": 3,
|
||||
"skips": 0,
|
||||
"junitCumulativeSeconds": 1336.203287,
|
||||
"pestSeconds": 174.58,
|
||||
"externalWallSeconds": 181.54
|
||||
},
|
||||
"deltaFeatureMinusBaseline": {
|
||||
"junitCumulativeSeconds": 49.713691,
|
||||
"junitCumulativePercent": 3.86429,
|
||||
"pestSeconds": 6.54,
|
||||
"pestPercent": 3.89193,
|
||||
"externalWallSeconds": 5.87,
|
||||
"externalWallPercent": 3.341493,
|
||||
"budgetAuthoritative": false
|
||||
},
|
||||
"rawBaselineOnlySignatureIds": [
|
||||
"browserConfigMissing"
|
||||
],
|
||||
"correctedBaselineOnlySignatureIds": [],
|
||||
"featureOnly": [],
|
||||
"identicalClassificationCount": 40,
|
||||
"classificationChanged": [],
|
||||
"materialMessageChanged": []
|
||||
},
|
||||
"confidence": {
|
||||
"selectedFileCount": 1587,
|
||||
"selectionManifestSha256": "7f8b97a58bb1457b9302e5823469edc2026c989a69c28db4ff1b653a2dba9fa2",
|
||||
"normalizedFileListSha256": "7ae55fc102e9e3e0ab561e7c73e1b8955890073a96ced4a7304aa0bf5ab88e2c",
|
||||
"identityCount": 6120,
|
||||
"identitySha256": "67cf6fa1cee7c9c22d41aa18f304004d81e5b4de610888ea95fa4505f4e16f23",
|
||||
"rawBaseline": {
|
||||
"shellExit": 2,
|
||||
"tests": 6120,
|
||||
"assertions": 39544,
|
||||
"failures": 96,
|
||||
"errors": 25,
|
||||
"skips": 26,
|
||||
"junitCumulativeSeconds": 4265.128724,
|
||||
"pestSeconds": 547.79,
|
||||
"externalWallSeconds": 580.62
|
||||
},
|
||||
"rawFeature": {
|
||||
"shellExit": 2,
|
||||
"tests": 6120,
|
||||
"assertions": 41830,
|
||||
"failures": 95,
|
||||
"errors": 23,
|
||||
"skips": 26,
|
||||
"junitCumulativeSeconds": 4217.546503,
|
||||
"pestSeconds": 540.79,
|
||||
"externalWallSeconds": 575.92
|
||||
},
|
||||
"deltaFeatureMinusBaseline": {
|
||||
"junitCumulativeSeconds": -47.582221,
|
||||
"junitCumulativePercent": -1.11561,
|
||||
"pestSeconds": -7.0,
|
||||
"pestPercent": -1.277862,
|
||||
"externalWallSeconds": -4.7,
|
||||
"externalWallPercent": -0.80948,
|
||||
"budgetAuthoritative": false
|
||||
},
|
||||
"rawBaselineOnlySignatureIds": [
|
||||
"browserConfigMissing",
|
||||
"livewireManifest",
|
||||
"platformRelocation"
|
||||
],
|
||||
"correctedBaselineOnlySignatureIds": [
|
||||
"livewireManifest",
|
||||
"platformRelocation"
|
||||
],
|
||||
"featureOnly": [],
|
||||
"identicalClassificationCount": 118,
|
||||
"classificationChanged": [],
|
||||
"materialMessageChanged": []
|
||||
},
|
||||
"browserPreconditionCorrection": {
|
||||
"configSha256Both": "6dd56bbb76fcba3bbc177dbcc6aeaad8ec2e0cbd6e61d4ebd40ef79139c772e6",
|
||||
"identitySha256Both": "89203c0d819931ff791eddbbcf74ba592ba29c974eeed59e04bf4a30b965f17a",
|
||||
"baseline": {
|
||||
"shellExit": 0,
|
||||
"tests": 1,
|
||||
"assertions": 2279,
|
||||
"status": "passed"
|
||||
},
|
||||
"feature": {
|
||||
"shellExit": 0,
|
||||
"tests": 1,
|
||||
"assertions": 2283,
|
||||
"status": "passed"
|
||||
}
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
"performanceAndBudgetAttribution": {
|
||||
"fastFeedback": {
|
||||
"featureRegressionConfirmed": false,
|
||||
"baselineDebt": false,
|
||||
"activationBlocker": false,
|
||||
"repeatRequired": false,
|
||||
"reason": "Both native runs are below the unchanged hard threshold; one small paired delta does not prove causal regression under P-001 and does not affect the merge decision"
|
||||
},
|
||||
"confidence": {
|
||||
"featureRegressionConfirmed": false,
|
||||
"baselineDebt": true,
|
||||
"baselineDebtType": "advisory budget and shared functional failures",
|
||||
"activationBlocker": false,
|
||||
"repeatRequired": false,
|
||||
"reason": "Both native and normalized runs exceed the same unchanged soft-warn threshold; feature is not slower and the advisory breach is not a hard gate"
|
||||
}
|
||||
},
|
||||
"artifactChecksums": {
|
||||
"nativeFastFeedback": {
|
||||
"baseline": {
|
||||
"terminal": "928b105cdf84e4510c1d5556cd9655d455ed6c58d7dc210abebf68b0d3ea8f74",
|
||||
"junit": "9ab6ef1917248a1065b6f814458741b8fa6be0bb232ef1f56933aceb8412a030",
|
||||
"report": "7f6d0a08dc0e60a6dad8aebb9bf5803b21bbbcf8708c45026f693e003c331a67",
|
||||
"budget": "041aa8174636d2e1b5cf6fa0916d98b25c7af6ee65b5c2077e479051c8a757e4",
|
||||
"profile": "62ee7000feaf76d0fd339e7effc247ca4e82d6992258a6767275db3a3c5a7c45",
|
||||
"summary": "057ab8ff68ac8e32456847cddda32881700ac331af05a5df905a27cca8a948fe",
|
||||
"trend": "d9ae9f4408fe72b4161929829c7749ee4c1e8cad557fdfdaae95cd4a4443e5e3"
|
||||
},
|
||||
"feature": {
|
||||
"terminal": "61f5a63f4f7e2144f64e78f05fc92f9c46aea601f713d1bd2414cd69755c25d8",
|
||||
"junit": "2b500d8ec964be50b5e2ea5fc0795b37e8e6f97d2cab9edd438e436525bd00fd",
|
||||
"report": "6eac0d4758720753e15fe673c005727a9bacb8b80f6539d688584727296a55aa",
|
||||
"budget": "6512fa137e29ede17459866c1cfe485367f761fba9d4eaecd20270a8e9da8e3d",
|
||||
"profile": "a145de74d9b18f86ee7b9b7674d35e8453f19cdbc84fbf520963efd867954fdc",
|
||||
"summary": "c6d707274afa46bb213df9a60ab8df123270488af498e2c4ad47d94be76096d1",
|
||||
"trend": "2beaeb937c2dddfb0421a4171a85fb5b3ee73ce99a1627f18a58a330577822a9"
|
||||
}
|
||||
},
|
||||
"nativeConfidence": {
|
||||
"baseline": {
|
||||
"terminal": "ef35435e750fc8c9a06f2ec0822f1eb99d74ae40573cdaeccd610e5f7192d9b1",
|
||||
"junit": "dcef66bde0853bacd9a39c70cea49aac1f3c0b9e60137af868e1f06c5035b8aa",
|
||||
"report": "cda680c570020dbb7a62129e27ec30a8b85c64d9f71c0d0fb8442f6557ec59cc",
|
||||
"budget": "e60cd6da81cb7d5194c0583431bffafc36d17e59d055f9e8059f85307ff086d4",
|
||||
"profile": "da0c0252b610cae173ae63f71102887410cf30637fabee5d771f91a861585777",
|
||||
"summary": "3eeb6394d1061b4a67bc28b17b3afdf4f5633e74cbafb82f19a5e9f2af6f65f0",
|
||||
"trend": "2a55d0474a608d7ae1fd215f4ed32cccaa2c6e95d5c6608d0c7fea6895eba938"
|
||||
},
|
||||
"feature": {
|
||||
"terminal": "6502c11e5a05767fc8e60d1fd8048264f4905f42dc64d9e9022d4f5a24683f6f",
|
||||
"junit": "b77a6fdc0369b7c4cb47c28834f1a15389b93351997298ad1441b7a174653304",
|
||||
"report": "f97dab2fd88de0ddfe71a02e6c8e539281e5e1fefb06ca05a697fef826cfb2b0",
|
||||
"budget": "2d24b6da8bc9857fa95f2f44dbd5a3b3d31c70c01efff0170762b428bb5edb5b",
|
||||
"profile": "47ccd23198856098f77ff04ed10cba64bc23b7f1f2ee487a772b8b9f9800b434",
|
||||
"summary": "24d7bd1ce1660298b0ab706ae0c547950ed0e0a565715c2257ef8d1a258644f2",
|
||||
"trend": "30da4989899311c521d94596cf30bb05d3487a9e3ba8d0f927af87c510ea626f"
|
||||
}
|
||||
},
|
||||
"normalized": {
|
||||
"fastFeedbackBaselineJunit": "91dde9c8487d02d7d6f0b7ce1f0f7cbbb0aecdf1a9af81a42b77140d1a2889b6",
|
||||
"fastFeedbackFeatureJunit": "05197c984d1ce852e41aa1b3c460a902b77368dd85cb191e7cb0b4381743a9de",
|
||||
"confidenceBaselineJunit": "3072beccdf25ddb36220c8f44db3e9e74d0e33462fff4339c054023e658ff9be",
|
||||
"confidenceFeatureJunit": "06b243e1f3424bbcccfce2f29cdf7fa0845908dbd3f47b8e3ba438619bb07f2d",
|
||||
"browserBaselineJunit": "b892fc260814758dc17bc2125779c202ed5a76632da13dae0e35e63d32f6194c",
|
||||
"browserFeatureJunit": "f04ddbae563620657299fc6d098a5b817646fac6b3287c22c40e0ca8d12c74d1"
|
||||
}
|
||||
},
|
||||
"comparabilityVerdict": "VALID",
|
||||
"attributionVerdict": "NO SPEC 439 FUNCTIONAL REGRESSION",
|
||||
"technicalRemediationPerformed": false,
|
||||
"activationAssessment": {
|
||||
"fastFeedbackHardGateBlocker": false,
|
||||
"fastFeedbackProcessFailureContract": true,
|
||||
"liveRequiredMergeCheckStatus": "unverified-external",
|
||||
"confidenceAdvisoryBaselineDebt": true,
|
||||
"localMergeReady": true,
|
||||
"externalCondition": "Before merge, verify live Gitea PR target context, checkout history, merge base, failed-lane artifacts, and that the red shared-baseline check is not a required merge boundary; if it is required, restore the Fast Feedback baseline first"
|
||||
},
|
||||
"finalVerdict": "PASS WITH EXTERNAL ACTIVATION CONDITION — MERGE-READY LOCALLY"
|
||||
}
|
||||
@ -0,0 +1,293 @@
|
||||
# Implementation Report: Spec 439 - Branch Topology and Local Evidence Truth
|
||||
|
||||
**Recorded**: 2026-07-11
|
||||
**Spec verdict**: **PASS WITH EXTERNAL ACTIVATION CONDITION**
|
||||
**Merge decision mapping**: **MERGE-READY LOCALLY**
|
||||
|
||||
The bounded Spec 439 implementation has been executed and its final reconciliation is recorded here. Fresh paired runs against the exact clean pre-439 baseline show no Feature-only failure/error case, changed classification, materially changed failure cause, or adverse budget activation. Both native lanes still return exit `2`; they remain failed lane executions and are not relabeled as PASS. The same normalized functional failures are present on the clean baseline under the same selection and environment, while Fast Feedback remains within its hard budget on both sides and Confidence exceeds the same advisory budget on both sides. `README.md` and the active `pull_request` workflow prove blocking process semantics, but repo files alone do not prove whether that check is currently configured as a live required Gitea merge boundary. That required-check status is the external activation condition and is not guessed.
|
||||
|
||||
## Repository Snapshot
|
||||
|
||||
| Field | Recorded truth |
|
||||
|---|---|
|
||||
| Branch | `feat/439-branch-topology-local-evidence-truth` |
|
||||
| HEAD | `71e3745aee7e69dbe2479ef622075d73c36b717a` |
|
||||
| Active package | `specs/439-branch-topology-local-evidence-truth/` |
|
||||
| Dirty state at Phase 8 entry | Intentional Spec 439 worktree: 17 tracked modified paths and 4 untracked status entries; the Spec entry contained four files before this report was added |
|
||||
| Dirty state at baseline-attribution entry | 18 tracked modified paths and 8 untracked files across 26 exact paths; tested worktree digest `a0755a5c650cb2704a1d2d47d8c4f616526b391da9adc6c91f4c3f3818b713a5` |
|
||||
| Dirty state after evidence reconciliation | 18 tracked modified paths and 9 untracked files across 27 exact paths, including `evidence/baseline-attribution.json`; no path outside the existing Spec 439 scope was introduced |
|
||||
| Local `dev` / `origin/dev` | `55338a88c69044c632cb006b7e7b066fbd2659b9` / same |
|
||||
| Local `platform-dev` / `origin/platform-dev` | `71e3745aee7e69dbe2479ef622075d73c36b717a` / same |
|
||||
| Local `website-dev` / `origin/website-dev` | `af5fa3034133942a4fcd43d5ba3cfdd975795869` / same |
|
||||
| Resolver result in this repository | `platform`, `repository-governance`, source `spec-metadata`, expected `platform-dev`, selected `origin/platform-dev`, target/merge base `71e3745...`, range `71e3745...HEAD` |
|
||||
| Exact clean baseline | `origin/platform-dev` = `platform-dev` = merge base = `71e3745aee7e69dbe2479ef622075d73c36b717a` |
|
||||
| Isolated baseline worktree | Detached clean worktree at `/Users/ahmeddarrazi/Documents/projects/wt-plattform-spec439-baseline-71e3745a`; held unchanged through evidence capture and removed after final validation |
|
||||
| Remote operations | No fetch, push, PR creation, remote mutation, branch-protection call, merge, rebase, or long-lived branch synchronization was executed |
|
||||
|
||||
The `git fetch` line added to the checked-in PR workflow is a future runner declaration; it was not executed during this local implementation loop.
|
||||
|
||||
## Constitution Amendment and Phase Gate
|
||||
|
||||
- The implementation process record states that the Constitution amendment was the first mutation after the read-only Phase 0 preflight.
|
||||
- It changed only the Sync Impact Report, the `Spec-First Workflow` branch rule, and version/date metadata.
|
||||
- Version changed from `2.16.0` to `3.0.0`. The decision is **MAJOR** because the former normative universal-`dev` rule was redefined incompatibly.
|
||||
- `Ratified` remains `2026-01-03`; `Last Amended` is `2026-07-11`.
|
||||
- The three roles are canonical: `dev` for explicit repository-wide integration/promotion, `platform-dev` for ordinary platform work, and `website-dev` for ordinary website work.
|
||||
- Wrong-family, `dev`/`origin/dev`, `HEAD~1`, and arbitrary-commit stream fallbacks are forbidden.
|
||||
- The unrelated normalized Constitution body is locked by SHA-256 `9ea4019798f1854e7b513a8fe0bce2529a19207d0b16e81a08582e6b661b8e74`.
|
||||
- The process record states that the Phase 1 gate passed before `Agents.md` or dependent resolver/evidence files were released and that T007-T061 remained blocked until that result.
|
||||
- Exact contract tests protect the amendment content, required metadata, authorized boundary, unrelated normalized body hash, and dependent-instruction synchronization. No final repository file conflicts with the Constitution effective after Phase 1.
|
||||
- The amendment-first sequence is process-attested and corroborated by the final repository state, exact amendment-boundary tests, synchronized dependent artifacts, and the absence of a conflicting final state. No independent historical commit boundary was required before the implementation began; none is retroactively claimed.
|
||||
- Under the corrected Amendment-first Evidence Sufficiency contract, FR-005, INV-001, INV-002, SC-002, and T056 are satisfied without weakening the prospective Phase 1 hard gate.
|
||||
|
||||
Impacted artifacts were the Constitution, the tracked authoritative instruction file `Agents.md`, the active Spec 439 package, the resolver/guard consumers, and evidence contracts. Specs 437 and 438 were reviewed but not changed.
|
||||
|
||||
## Changed-File Traceability
|
||||
|
||||
| File | Reason | Requirements / invariants |
|
||||
|---|---|---|
|
||||
| `.specify/memory/constitution.md` | Narrow MAJOR branch-topology amendment and required metadata | FR-001-FR-005, FR-007-FR-013; INV-001-INV-009 |
|
||||
| `Agents.md` | Remove directly conflicting universal-`dev` routing and synchronize canonical roles | FR-006, FR-008-FR-012, FR-033; INV-002, INV-004-INV-009 |
|
||||
| `.gitea/workflows/test-pr-fast-feedback.yml` | Remove hard-coded wrong target, pass actual PR target, fetch matching ref/history, fail closed | FR-031, FR-032, FR-051, FR-053; INV-008-INV-012, INV-020 |
|
||||
| `scripts/resolve-feature-base` | Implement the fixed two-stage family/ref/merge-base resolver | FR-014-FR-028; INV-007-INV-012, INV-020 |
|
||||
| `scripts/check-ui-productization-coverage` | Consume resolver output while preserving all working-tree sources | FR-024, FR-029-FR-031; INV-008, INV-010-INV-012 |
|
||||
| `scripts/validate-ui-productization-coverage-guard` | Deterministic resolver/guard integration and fail-before-diff fixtures | FR-029-FR-031, FR-051; INV-010-INV-012, INV-020 |
|
||||
| `scripts/platform-test-lane` | Carry workflow/trigger context through Sail and propagate the canonical final exit | FR-034-FR-038, FR-043-FR-049; INV-013-INV-017 |
|
||||
| `scripts/platform-test-report` | Carry refresh workflow/trigger context and propagate the canonical final exit | FR-035-FR-049; INV-013-INV-017 |
|
||||
| `docs/ui-ux-enterprise-audit/README.md` | Document family-aware guard usage and the live-Gitea residual truthfully | FR-029-FR-033, FR-053 |
|
||||
| `apps/platform/tests/Support/TestLaneReport.php` | Persist execution/evidence, validate JUnit/report/artifact truth, centralize final result, invalidate stale publications, and roll final artifact writes back to one provisional fail-closed bundle | FR-034-FR-049; INV-013-INV-017 |
|
||||
| `apps/platform/tests/Support/TestLaneManifest.php` | Preserve execution through run/refresh, validate wrapper trigger/context, use one final-result path | FR-034-FR-049; INV-013-INV-017 |
|
||||
| `apps/platform/tests/Support/TestLaneBudget.php` | Fail closed on malformed authoritative profile inputs without changing profiles | FR-043-FR-048; INV-014-INV-017 |
|
||||
| `apps/platform/tests/Feature/Guards/BranchTopologyContractTest.php` | BS-001-BS-012 and supplemental resolver contract proof | FR-014-FR-028, FR-051; INV-007-INV-012, INV-020 |
|
||||
| `apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php` | Roles, metadata, exact amendment boundary, and instruction synchronization | FR-001-FR-013; INV-001-INV-009 |
|
||||
| `apps/platform/tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php` | Prove dynamic target declaration, full history, and no hard-coded `origin/dev` | FR-031, FR-032, FR-049, FR-053; INV-008-INV-012 |
|
||||
| `apps/platform/tests/Feature/Guards/CiLaneFailureClassificationContractTest.php` | EF-003/EF-004 and failure-class/final-result controls | FR-038, FR-043-FR-049; INV-014-INV-017 |
|
||||
| `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` | Deterministic truthful artifact bundle and copy-failure controls | FR-038-FR-041, FR-049, FR-051; INV-016, INV-017, INV-020 |
|
||||
| `apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php` | Wrapper propagation, CI context, wrong-lane and trigger-conflict controls | FR-034-FR-049, FR-051; INV-013-INV-017, INV-020 |
|
||||
| `apps/platform/tests/Feature/Guards/TestLaneTrendClassificationTest.php` | Prevent a stable over-budget run from being mislabeled `budget-near` or described as under budget | FR-040, FR-043-FR-046, FR-049; INV-014-INV-016 |
|
||||
| `apps/platform/tests/Unit/Support/TestLaneBudgetTest.php` | Missing/malformed profile and measured-time controls | FR-043-FR-048; INV-014-INV-017 |
|
||||
| `apps/platform/tests/Unit/Support/TestLaneReportTest.php` | EF-001/002/005/006/007 plus refresh, contradiction, write, shape, and wrapper controls | FR-034-FR-042, FR-045-FR-049; INV-013-INV-017 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/spec.md` | Corrected authoritative feature requirements | FR-001-FR-054; INV-001-INV-020 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/plan.md` | Implementation design, phase gates, paths, tests, and residual policy | FR-001-FR-054; INV-001-INV-020 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/tasks.md` | Dependency-ordered execution and evidence status | FR-001-FR-054; INV-001-INV-020 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/checklists/requirements.md` | Requirements-quality snapshot, explicitly separated from live implementation state | FR-054 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/implementation-report.md` | Final traceability, evidence, residuals, and verdict | FR-054; SC-012 |
|
||||
| `specs/439-branch-topology-local-evidence-truth/evidence/baseline-attribution.json` | Compact paired baseline/feature environment, selection, JUnit multiset, performance, budget, and artifact attribution | FR-054; SC-009, SC-012 |
|
||||
|
||||
No `apps/platform/composer.json` change was necessary. No product runtime, website runtime, config, provider, route, view, migration, model, policy, job, or customer-output file changed.
|
||||
|
||||
## BS Fixture Matrix
|
||||
|
||||
Fixture SHAs are generated inside isolated temporary repositories. “Fixture SHA” below means the actual 40-hex value produced in that test and compared for exact equality; it is intentionally not a stable repository SHA.
|
||||
|
||||
| ID | Topology and signals | Expected | Actual result |
|
||||
|---|---|---|---|
|
||||
| BS-001 | Platform, explicit family; local and `origin/platform-dev` exist | Remote ref; target = merge base; feature range | `platform`, `origin/platform-dev`, target/merge-base = fixture SHA, `<merge-base>...HEAD`; exit 0 |
|
||||
| BS-002 | Platform, explicit family; remote absent, local `platform-dev` exists | Local family ref only | `platform`, `platform-dev`, target/merge-base = fixture SHA, `<merge-base>...HEAD`; exit 0 |
|
||||
| BS-003 | Website, explicit family; local and `origin/website-dev` exist | Remote ref; target = merge base | `website`, `origin/website-dev`, target/merge-base = fixture SHA, `<merge-base>...HEAD`; exit 0 |
|
||||
| BS-004 | Website, explicit family; remote absent, local `website-dev` exists | Local family ref only | `website`, `website-dev`, target/merge-base = fixture SHA, `<merge-base>...HEAD`; exit 0 |
|
||||
| BS-005 | Explicit repository-wide family + `promotion` + PR target `dev` | `dev` accepted only after classification | `repository-wide`, `promotion`, `origin/dev`, target/merge-base = fixture SHA; exit 0 |
|
||||
| BS-006 | Platform family; only `dev`/`origin/dev` available | Fail; no fallback or diff | `reason=missing-family-ref`, attempted `origin/platform-dev,platform-dev`, guidance present, no target/merge-base/range, `diff_ran=false`; exit 2 |
|
||||
| BS-007 | Website family; platform and dev refs available, website refs absent | Fail; no wrong-family/dev fallback | `reason=missing-family-ref`, attempted `origin/website-dev,website-dev`, guidance present, no target/merge-base/range, `diff_ran=false`; exit 2 |
|
||||
| BS-008 | Diverged platform target and feature histories | Merge base, not target tip; feature path only | Selected `origin/platform-dev`; merge base differs from target; changed path exactly `platform-feature.txt`; exit 0 |
|
||||
| BS-009 | Diverged website target and feature histories | Merge base, not target tip; feature path only | Selected `origin/website-dev`; merge base differs from target; changed path exactly `website-feature.txt`; exit 0 |
|
||||
| BS-010 | Explicit platform family + PR target `website-dev` | Actionable conflict before diff | `reason=branch-family-conflict`, explicit `platform`, target family `website`, guidance, `diff_ran=false`; exit 2 |
|
||||
| BS-011 | Branch `feat/platform-fixture`, repo scope `website`, readable spec without branch metadata | Ambiguous classification failure | `reason=ambiguous-branch-family`, all three diagnostic inputs, guidance, `diff_ran=false`; exit 2 |
|
||||
| BS-012 | Platform family + explicit `origin/website-dev` ref | Wrong-family rejection before merge-base | `reason=wrong-family-ref`, expected `platform-dev`, explicit wrong ref, no target/merge-base/range, `diff_ran=false`; exit 2 |
|
||||
|
||||
Focused result: all twelve named fixtures plus the supplemental priority/diagnostic/portability contract passed inside `BranchTopologyContractTest`.
|
||||
|
||||
## EF Fixture Matrix
|
||||
|
||||
| ID | Execution / evidence / budget input | Expected final truth | Actual result |
|
||||
|---|---|---|---|
|
||||
| EF-001 | Execution exit 7, passing JUnit, then refresh | Stored failure cannot become pass | Failed before/after refresh; final exit 7; original execution retained |
|
||||
| EF-002 | Execution exit 0 + valid passing JUnit | Compatible pass remains pass | Passed before/after refresh; final exit 0 |
|
||||
| EF-003 | Fast Feedback PR profile, measured 216 s > 215 s hard threshold | Blocking breach | `blocking-budget-breach`, blocking, final exit 1 |
|
||||
| EF-004 | Confidence mainline profile, measured 481 s > 480 s soft threshold | Visible advisory only | `budget-advisory-warning`, non-blocking warning, final exit 0 absent another failure |
|
||||
| EF-005 | Execution exit 0 + JUnit `<failure>` or `<error>` | Contradiction and non-zero | `junit-failures`/`junit-errors` plus `evidence-contradiction`; final exit 1 |
|
||||
| EF-006 | Missing or malformed required JUnit | Evidence failure | `evidence-missing`/`evidence-malformed`; final exit 1 |
|
||||
| EF-007 | Execution exit 9 + report-generation failure | Both contexts retained; original non-zero wins | `test-failure`, `report-generation-failure`, and contradiction visible; final exit 9 |
|
||||
|
||||
Supplemental evidence controls also pass for unknown/current workflow preservation, wrong-lane binding, trigger/profile mismatch, malformed/scalar/list report input, required artifact write/copy failure, refresh publication failure, non-finite/negative measured time, deterministic bundle contents, final-status Markdown rendering, stale-result invalidation, and injected failure at each of the three final publication writes. If a final publication write fails, summary, budget, and report are all restored to the same provisional failure payload rather than leaving a stale or partial PASS.
|
||||
|
||||
## Validation Results
|
||||
|
||||
### Focused proof
|
||||
|
||||
| Command / proof | Result |
|
||||
|---|---|
|
||||
| Sail focused final set: Constitution, branch topology, report, budget, failure classification, wrappers, artifacts, manifest, trend/recalibration, and Fast/Confidence workflow contracts | **PASS: 77 tests, 1,404 assertions; exit 0** |
|
||||
| `scripts/validate-ui-productization-coverage-guard` | **PASS: 15/15 cases**, including committed/staged/unstaged/untracked paths and resolver fail-before-diff |
|
||||
| Real-repository `scripts/resolve-feature-base --format=kv` | **PASS**, selected `origin/platform-dev` with merge-base range |
|
||||
| Shell syntax for resolver, guard, lane/report/artifact wrappers, validator | **PASS** |
|
||||
| Ruby YAML parse for `.gitea/workflows/test-pr-fast-feedback.yml` | **PASS** |
|
||||
| Sail Pint on all 12 changed/new PHP files, followed by the final 4-file touched subset | **PASS**, no remaining style changes |
|
||||
| `git diff --check` | **PASS** |
|
||||
| Forbidden runtime/UI path scan | **PASS: 0 paths** |
|
||||
| Specs 437/438 diff | **PASS: empty** |
|
||||
|
||||
The final focused invocation was run from `apps/platform` and returned exit `0`:
|
||||
|
||||
```text
|
||||
./vendor/bin/sail artisan test tests/Feature/Guards/BranchTopologyContractTest.php tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php tests/Unit/Support/TestLaneReportTest.php tests/Unit/Support/TestLaneBudgetTest.php tests/Feature/Guards/CiLaneFailureClassificationContractTest.php tests/Feature/Guards/TestLaneCommandContractTest.php tests/Feature/Guards/TestLaneArtifactsContractTest.php tests/Feature/Guards/TestLaneManifestTest.php tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php tests/Feature/Guards/CiConfidenceWorkflowContractTest.php tests/Feature/Guards/TestLaneTrendClassificationTest.php tests/Feature/Guards/TestLaneTrendSummaryContractTest.php tests/Feature/Guards/TestLaneTrendLogicalContractTest.php tests/Feature/Guards/TestLaneTrendContractSchemaTest.php tests/Feature/Guards/TestLaneRecalibrationEvidenceContractTest.php tests/Feature/Guards/TestLaneRecalibrationPolicyTest.php
|
||||
```
|
||||
|
||||
This one consolidated proof contains every file named by T048-T050. Its terminal result was `77 passed (1,404 assertions)` in `17.00s`.
|
||||
|
||||
### Required actual lanes
|
||||
|
||||
The exact pre-439 baseline was tested in a detached clean worktree under a separate Compose project. Baseline and feature used the same Laravel/Pest container image, PHP `8.4.15`, Laravel `12.52.0`, Pest `4.3.1`, PHPUnit `12.5.4`, PostgreSQL server `16.11`, architecture `arm64`, eight CPUs, container timezone `UTC`, Composer-lock checksum `ae033b4...`, PHPUnit configuration checksum `db1a...`, workflow IDs, trigger classes, and lane commands. The lanes use in-memory SQLite; no persistent PostgreSQL test data participated.
|
||||
|
||||
| Side | Lane | Shell exit | JUnit tests | Failures | Errors | Skips | Assertions | Lane wall | Budget | Artifacts / report |
|
||||
|---|---|---:|---:|---:|---:|---:|---:|---:|---|---|
|
||||
| Clean baseline | Fast Feedback | 2 | 2,825 | 39 | 5 | 0 | 17,404 | 187.344327 s | within `200 + 15 = 215` s hard profile | complete / failed |
|
||||
| Spec 439 | Fast Feedback | 2 | 2,865 | 37 | 3 | 0 | 20,511 | 191.234712 s | within `200 + 15 = 215` s hard profile | complete / failed |
|
||||
| Clean baseline | Confidence | 2 | 6,144 | 97 | 26 | 26 | 39,731 | 614.802711 s | over `450 + 30 = 480` s soft profile | complete / failed |
|
||||
| Spec 439 | Confidence | 2 | 6,184 | 95 | 23 | 26 | 42,837 | 590.987661 s | over `450 + 30 = 480` s soft profile | complete / failed |
|
||||
|
||||
All four native process exits and matching JUnit documents were captured independently of generated report claims. The pre-439 wrapper does not propagate host-exported CI context into the existing Sail container, so its recorded report evaluates the generic local profile. For attribution, the raw exit and JUnit are primary evidence and both baseline durations were additionally evaluated against the unchanged authoritative workflow/trigger profiles shown above.
|
||||
|
||||
The exact required-lane invocations were run from the repository root:
|
||||
|
||||
```text
|
||||
./scripts/platform-test-lane fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request
|
||||
./scripts/platform-test-lane confidence --workflow-id=main-confidence --trigger-class=mainline-push
|
||||
```
|
||||
|
||||
Each invocation returned exit `2`. The counts, durations, budget outcomes, and artifact-completeness values in the table come from those final invocations and their matching persisted report/JUnit bundles, not from an earlier run.
|
||||
|
||||
### Exact failure/error attribution
|
||||
|
||||
Normalized failure/error signatures use testsuite, classname, testcase name, failure/error classification, type, and the first materially relevant message. They are compared as multisets.
|
||||
|
||||
| Native lane | Baseline-only | Feature-only | Identical | Classification changed | Material message changed |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| Fast Feedback | 4 | 0 | 40 | 0 | 0 |
|
||||
| Confidence | 5 | 0 | 118 | 0 | 0 |
|
||||
|
||||
The native selector resolves 679/681 Fast Feedback files and 1,594/1,596 Confidence files because Spec 439 adds two contract files. At testcase-identity level each feature lane has 42 new or renamed passing identities and omits two superseded baseline identities, for net `+40` tests. That selection difference is expected but cannot establish parity by itself.
|
||||
|
||||
The supplemental attribution therefore excluded all nine changed Pest files on both sides and ran the resulting identical selections directly:
|
||||
|
||||
| Normalized lane | Files / selection hash | Test identities | Raw baseline-only | Corrected baseline-only | Feature-only | Identical outcomes | Classification / message changed |
|
||||
|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| Fast Feedback | 672 / `c8d47eb7...` | 2,801 | 1 | 0 | 0 | 40 | 0 / 0 |
|
||||
| Confidence | 1,587 / `7f8b97a5...` | 6,120 | 3 | 2 | 0 | 118 | 0 / 0 |
|
||||
|
||||
The one raw Fast and one raw Confidence asymmetry was the same `BrowserLaneIsolationTest`: the baseline worktree lacked the generated `browser-lane.phpunit.xml`, while the feature worktree already had it. After generating the baseline artifact through the repository's own command, both browser configs were byte-identical (`6dd56bbb...`) and the targeted testcase passed on both sides. The two remaining normalized Confidence baseline-only outcomes are isolated baseline/worktree failures in `LivewireInterceptShimTest` and `PlatformRelocation/RuntimeSmokeTest`; neither is Feature-only. Large rendered HTML, CSRF/Livewire IDs, factory values, and Mockery ordinals were normalized only after the status, exception type, assertion operator, and expected semantic value were compared.
|
||||
|
||||
Feature-only or changed failure/error cases: **none**.
|
||||
|
||||
The same normalized failure/error cases are present on the clean pre-Spec-439 baseline under the same lane selection and environment. No new functional lane regression was detected. The red shared product-domain outcomes remain baseline debt and were not repaired or reclassified inside Spec 439.
|
||||
|
||||
## Budget, Baseline, and Trend Truth
|
||||
|
||||
- `TestLaneBudget::enforcementProfiles()` is byte-identical between HEAD and the worktree: SHA-256 `6e1d54496ff36d8d8bc8596e4520261f65917d2615e8f640d2d363b6e23145dc` in both.
|
||||
- Fast Feedback keeps `200 + 15 = 215` seconds and `hard-fail`.
|
||||
- Confidence keeps `450 + 30 = 480` seconds and `soft-warn`.
|
||||
- No threshold was relaxed and no profile was reclassified.
|
||||
- Fast Feedback native wall time changed from `187.344327` to `191.234712` seconds (`+3.890385`, `+2.08%`). Both sides remain within the hard profile. Under P-001 this single small difference does not prove a causal performance regression; P-002 does not require repetition because no limit or merge decision depends on it.
|
||||
- The normalized Fast Feedback direct-run pair records JUnit cumulative `+49.713691` s (`+3.86%`), Pest `+6.54` s (`+3.89%`), and external wall `+5.87` s (`+3.34%`). These direct-run metrics support identical-selection attribution; only native lane wall time is budget-authoritative.
|
||||
- Confidence native wall time changed from `614.802711` to `590.987661` seconds (`-23.815050`, `-3.87%`). Both sides exceed the unchanged advisory profile. The normalized identical-selection pair records JUnit cumulative `-47.582221` s (`-1.12%`), Pest `-7.00` s (`-1.28%`), and external wall `-4.70` s (`-0.81%`). These direct-run metrics are attribution evidence, not the budget-authoritative lane metric. This is pre-existing advisory baseline debt, not a confirmed feature regression or activation blocker.
|
||||
- Confidence remains `soft-warn`; its budget classification is non-blocking, but both native Confidence executions remain functionally failed and are never described as lane PASS.
|
||||
- No Fast Feedback hard-budget debt or hard-gate activation blocker was reproduced. No second performance pair is required.
|
||||
- Required summary, budget, report, JUnit, trend-history, terminal, normalized-selection, and targeted-precondition artifacts were captured separately for baseline and feature. Compact checksums and the exact attribution are in `evidence/baseline-attribution.json`.
|
||||
|
||||
## Historical and Scope Safety
|
||||
|
||||
| Protected surface | Evidence |
|
||||
|---|---|
|
||||
| Spec 437 tree | Combined file-list/content digest `100ab3732d550f9e437fd25a15f1bbe2cf01087c7e9bad3ef07072dcc974a663`; `git diff --exit-code` = 0 |
|
||||
| Spec 438 tree | Combined file-list/content digest `9ec7573649e9249e3e9c2f2f6813405c5a0ad5c5a0960830da49713d13445f94`; `git diff --exit-code` = 0 |
|
||||
| Product runtime | No changes under `apps/platform/app`, `config`, `database`, `routes`, or `resources` |
|
||||
| Website runtime | No changes under `apps/website/src` |
|
||||
| CI scope | Existing PR workflow only; triggers, jobs, lane, artifact upload, and schedules are unchanged |
|
||||
| Remote governance | No branch protection, merge gate, deployment activation, or Gitea administration |
|
||||
|
||||
## Product Surface and Deployment Close-Out
|
||||
|
||||
- **No-legacy posture**: canonical replacement of invalid branch/result defaults; no compatibility fallback for false-success or wrong-family behavior.
|
||||
- **Product Surface Impact**: N/A - no rendered surface changed.
|
||||
- **UI Surface Impact**: N/A - no page, route, navigation, table, form, modal, action, or customer output changed.
|
||||
- **Page archetype / surface budgets / Technical Annex / deep-link demotion / status vocabulary**: N/A.
|
||||
- **Product Surface exceptions**: none.
|
||||
- **Browser proof**: N/A - no rendered UI surface changed.
|
||||
- **Human Product Sanity**: N/A; visible product complexity is unchanged.
|
||||
- **Livewire v4 compliance**: unchanged; lockfile resolves `livewire/livewire v4.1.4` with Filament `v5.2.1`.
|
||||
- **Provider registration**: unchanged in `apps/platform/bootstrap/providers.php`; Admin and System panel providers remain registered there.
|
||||
- **Global search**: no resource or global-search behavior changed.
|
||||
- **Destructive/high-impact actions**: none added or changed; confirmation/authorization posture is unchanged.
|
||||
- **Assets**: none added or changed; no `filament:assets` deployment step is introduced by this feature.
|
||||
- **Deployment**: no environment variable, migration, queue, scheduler, storage, proxy, Sail runtime, Dokploy, staging, or production change. The checked-in PR validation declaration is repository CI metadata, not a deployed product change.
|
||||
- **Completed-spec rewrite safety**: Specs 437 and 438 remain byte-for-byte unchanged.
|
||||
|
||||
## Test-Governance Outcome
|
||||
|
||||
**Outcome: document-in-feature.** The bounded Unit/Feature fixtures and two required existing lanes are recorded here. No Heavy Governance, Browser, PostgreSQL, database, or remote-Gitea test family was added. Broad failing product tests are not silently imported into Spec 439 repair scope.
|
||||
|
||||
## External Residuals and Unrelated Drift
|
||||
|
||||
1. The PR workflow now uses `${{ github.base_ref }}`, a context namespace already used by repository Gitea workflows, and the YAML/contract are locally valid. A live PR must still verify target context, checkout history, merge base, and failed-lane artifact publication. No remote verification is claimed.
|
||||
2. `README.md` states that pull requests run Fast Feedback and block on process failures; `.gitea/workflows/test-pr-fast-feedback.yml` has an active `pull_request` trigger. These repo files do not prove live required-check/Branch-Protection configuration. Before merge, Gitea must confirm that the currently red shared-baseline check is not a required merge boundary; if it is required, merge must wait for separate baseline restoration.
|
||||
3. Fast Feedback and Confidence remain failed lane executions. Paired JUnit evidence proves zero Feature-only or changed functional outcome; the shared failures remain baseline debt and are not hidden behind unchanged file paths.
|
||||
4. Final publication failures are covered at each write position and normally roll all three result artifacts back to a coherent provisional failure bundle. On a pathological filesystem that rejects both the original final write and every subsequent rollback write/unlink, a partial file could remain; the process still exits non-zero, but no stronger atomic-filesystem guarantee is claimed.
|
||||
|
||||
## Baseline Debt Register
|
||||
|
||||
| Lane / debt | Clean-baseline evidence | Feature delta | Separate remediation candidate | Blocks Spec 439? |
|
||||
|---|---|---|---|---|
|
||||
| Fast Feedback shared functional failures | Native JUnit `9ab6ef19...`; 40 normalized outcomes also present in feature; repo PR trigger/process-failure contract in `README.md` and `.gitea/workflows/test-pr-fast-feedback.yml` | 0 Feature-only; 0 classification/message changes | **Fast Feedback Baseline Restoration** | No local Spec 439 blocker is proven. It blocks merge only if live Gitea config makes the failed check a required merge boundary; that is the explicit external condition. |
|
||||
| Confidence shared functional failures | Native JUnit `dcef66bd...`; 118 normalized outcomes also present in feature | 0 Feature-only; 0 classification/message changes | **Confidence Baseline Restoration** | No. The exact pre-439 baseline reproduces the same outcomes and Spec 439 adds no functional regression. |
|
||||
| Confidence advisory budget debt | Baseline `614.802711` s and feature `590.987661` s against unchanged 480 s `soft-warn` profile | No adverse feature delta; both over advisory threshold | **Confidence Budget Baseline Restoration** | No. The profile is advisory and no hard-gate activation occurs. The failed functional lane is nevertheless not called PASS. |
|
||||
| Isolated baseline-worktree prerequisites | Browser config initially absent; two additional Confidence baseline-only worktree/setup outcomes | 0 Feature-only; paired browser precondition passes on both sides | **Baseline Worktree Test Harness Restoration** | No. These are baseline-only setup/path outcomes, not Spec 439 regressions; the browser precondition was normalized for attribution only. |
|
||||
|
||||
## Requirement and Invariant Reconciliation
|
||||
|
||||
| IDs | Result | Evidence |
|
||||
|---|---|---|
|
||||
| FR-001-FR-004, FR-006-FR-007 | PASS for current repository content | Narrow MAJOR amendment, Sync Impact, date/version, and synchronized `Agents.md` content are repo-proven |
|
||||
| FR-005 | PASS under corrected evidence-sufficiency contract | Process attestation, final amendment, exact boundary/unrelated-scope tests, synchronized dependents, no conflicting final state, and explicit no-checkpoint status |
|
||||
| FR-008-FR-013 | PASS | Constitution/instructions and promotion/divergence fixtures define all roles without universal fallback |
|
||||
| FR-014-FR-020 | PASS | Stage 1 priority/conflict/fail-closed tests, BS-010/011, supplemental signal tests |
|
||||
| FR-021-FR-028 | PASS | Stage 2 ref priority, BS-001-009/012, diagnostics, merge-base paths |
|
||||
| FR-029-FR-033 | PASS locally; external verification residual | Resolver-backed guard, 15 cases, dynamic PR target workflow contract, no CI expansion |
|
||||
| FR-034-FR-042 | PASS | Stored execution, refresh, JUnit shapes/counts, contradictions, required write/report evidence |
|
||||
| FR-043-FR-049 | PASS | Unchanged authoritative profiles, blocking/advisory controls, shared final-result/wrapper path |
|
||||
| FR-050-FR-053 | PASS | Zero forbidden paths, local deterministic fixtures, protected specs, no remote/deployment activation |
|
||||
| FR-054 | PASS as reconciliation artifact | This report maps files, fixtures, tasks, tests, residuals, and verdict |
|
||||
| INV-001-INV-002 | PASS under corrected evidence-sufficiency contract | The prospective gate is unchanged; close-out is process-attested and final-state corroborated without retroactive historical proof |
|
||||
| INV-003 | PASS | Normalized unrelated Constitution body hash is locked |
|
||||
| INV-004-INV-009 | PASS | Scope-specific roles, valid explicit `dev`, no stream fallback, explicit promotion |
|
||||
| INV-010-INV-012 | PASS | One two-stage resolver, conflict failure, merge-base semantics |
|
||||
| INV-013-INV-017 | PASS | Execution immutability, blocking/advisory truth, evidence agreement, fail-closed missing evidence |
|
||||
| INV-018-INV-020 | PASS | No product runtime, Specs 437/438 untouched, local network-free proof |
|
||||
| SC-001, SC-003-SC-011 | PASS for the bounded feature contract | Amendment content, fixtures, focused proof, scope/history checks, and paired baseline attribution are recorded |
|
||||
| SC-002 | PASS under corrected evidence-sufficiency contract | Final amendment boundary and dependent-state consistency corroborate the process attestation; no pre-required checkpoint existed or is claimed |
|
||||
| SC-012 | PASS | Requirements, tasks, fixtures, paired lane/budget attribution, residuals, and exact verdict are reconciled without converting failed lanes into PASS |
|
||||
|
||||
All FR-001 through FR-054 and INV-001 through INV-020 are covered above. All 61 tasks are complete; 0 are open and 0 are blocked. T047, T053, T056, T059, and T061 were re-evaluated because of the evidence-contract correction and paired attribution. BS-001-BS-012 and EF-001-EF-007 each occur exactly once as named controls.
|
||||
|
||||
## Definition of Done Reconciliation
|
||||
|
||||
| Item | Result |
|
||||
|---|---|
|
||||
| Narrow Constitution amendment applied and current content validated | PASS |
|
||||
| Amendment-first evidence sufficiency satisfied without retroactive chronology claim | PASS |
|
||||
| Direct authoritative instructions synchronized | PASS |
|
||||
| Spec/plan/tasks/checklist/resolver/guard/workflow share one contract | PASS |
|
||||
| BS-001-BS-012 and EF-001-EF-007 recorded and green | PASS |
|
||||
| One truthful execution/evidence/report/budget final result | PASS |
|
||||
| Existing thresholds unchanged | PASS |
|
||||
| No forbidden runtime/UI/deployment/remote/historical-spec changes | PASS |
|
||||
| Focused tests, lanes, formatter, paired attribution, and diff checks recorded | PASS, with mandatory lanes honestly failed, zero functional failure/error regression delta, and no confirmed performance regression |
|
||||
| Implementation report content present | PASS |
|
||||
| T061 final-completion gate | PASS / COMPLETE |
|
||||
| Local merge readiness | PASS |
|
||||
| Live Gitea required-check state, context/history/artifacts | EXTERNAL ACTIVATION CONDITION |
|
||||
|
||||
## Final Decision
|
||||
|
||||
**PASS WITH EXTERNAL ACTIVATION CONDITION — MERGE-READY LOCALLY**
|
||||
|
||||
No Spec 439 functional regression, no confirmed performance regression, and no Fast Feedback hard-budget breach or specified local activation blocker was reproduced. The failed lane statuses and baseline debt remain explicit. The corrected chronology contract is satisfied without inventing historical evidence. Local merge criteria are complete. Before merge, live Gitea must confirm target context, checkout history, merge base, failed-lane artifact behavior, and that the currently red shared-baseline check is not configured as a required merge boundary; if it is required, merge must wait for separate Fast Feedback Baseline Restoration.
|
||||
558
specs/439-branch-topology-local-evidence-truth/plan.md
Normal file
558
specs/439-branch-topology-local-evidence-truth/plan.md
Normal file
@ -0,0 +1,558 @@
|
||||
# Implementation Plan: Spec 439 - Branch Topology and Local Evidence Truth
|
||||
|
||||
**Branch**: feat/439-branch-topology-local-evidence-truth | **Date**: 2026-07-10 | **Corrected**: 2026-07-11 | **Spec**: specs/439-branch-topology-local-evidence-truth/spec.md
|
||||
**Input**: Corrected Spec 439 branch-topology and engineering-evidence remediation.
|
||||
**Status**: Implemented and reconciled; merge-ready locally with an external activation condition.
|
||||
|
||||
**Lifecycle boundary**: Imperative phase text and readiness gates below preserve the approved preparation-time execution plan. Current task completion, evidence, residuals, and merge status are owned by `tasks.md` and `implementation-report.md`.
|
||||
|
||||
## Summary
|
||||
|
||||
Implement one bounded repository-governance slice that:
|
||||
|
||||
1. first replaces the obsolete universal dev branch rule through the current Constitution Amendment Procedure;
|
||||
2. validates that amendment before synchronizing directly conflicting authoritative instructions;
|
||||
3. resolves branch family separately from the concrete Git ref;
|
||||
4. maps ordinary platform work to platform-dev and ordinary website work to website-dev;
|
||||
5. permits dev only for explicitly classified repository integration/promotion;
|
||||
6. removes silent stream fallback to dev, origin/dev, or HEAD~1;
|
||||
7. preserves the original test process result through report refresh;
|
||||
8. makes existing blocking/advisory budget profiles affect the final command result correctly;
|
||||
9. validates required JUnit/report evidence and detects contradictions;
|
||||
10. proves the contract with deterministic local fixtures.
|
||||
|
||||
The implementation changes governance documents, repository tooling, tests, and existing local evidence/report wiring only. It does not change product runtime, rendered surfaces, data, RBAC, OperationRun behavior, deployment, or remote branch administration.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Markdown governance documents; Bash; PHP 8.4.15 for existing test-support code
|
||||
**Framework/Test Stack**: Laravel 12 repository, Pest 4, PHPUnit 12, Symfony Process already available
|
||||
**Storage**: Existing filesystem test artifacts only; no database changes
|
||||
**Project Type**: Monorepo with apps/platform, apps/website, root scripts, Spec Kit, and Gitea workflow declarations
|
||||
**Primary Constraints**: Constitution amendment first; Sail-first for PHP/Pest; no network-dependent fixtures; no product runtime; no new dependency; no generic branch graph
|
||||
**Performance Goal**: Focused fixtures remain small and deterministic; existing Fast Feedback and Confidence budgets remain the no-regression boundary
|
||||
**Scale/Scope**: Three known long-lived branches, fixed work classifications, existing local lane/report entry points
|
||||
|
||||
## Correction Preflight Evidence
|
||||
|
||||
Spec correction began on feat/439-branch-topology-local-evidence-truth at 71e3745a with only the existing untracked Spec 439 package.
|
||||
|
||||
Confirmed current repo truth:
|
||||
|
||||
- Constitution 2.16.0 still contains the universal dev branch rule.
|
||||
- The Amendment Procedure requires an explicit Constitution update PR, rationale, impacted templates/specs, SemVer decision, and Last Amended update.
|
||||
- No authoritative repo rule requires a separate preceding Amendment Spec.
|
||||
- Agents.md still contains directly conflicting universal dev instructions.
|
||||
- The current changed-file guard defaults to origin/dev and then HEAD~1.
|
||||
- The PR fast-feedback workflow explicitly fetches/passes origin/dev.
|
||||
- TestLaneManifest::renderLatestReport() supplies exitCode 0 and returns 0 unconditionally.
|
||||
- TestLaneReport::parseJUnit() currently extracts duration data only.
|
||||
- TestLaneBudget already owns trigger-aware blocking/advisory classifications.
|
||||
- Spec 438 remains deferred and unimplemented.
|
||||
|
||||
Implementation Phase 0 MUST revalidate these call paths and governance rules on current HEAD. Material drift requires reconciliation of the active artifacts before mutation.
|
||||
|
||||
## Governance Decision
|
||||
|
||||
### Amendment posture
|
||||
|
||||
The branch-topology amendment is inside Spec 439 scope and is Phase 1. It is not an external prerequisite.
|
||||
|
||||
The same Spec 439 feature PR may contain:
|
||||
|
||||
1. the narrow Constitution amendment;
|
||||
2. its required Sync Impact, rationale, impacted-artifact list, SemVer decision, and Last Amended update;
|
||||
3. directly dependent implementation after the Phase 1 validation gate.
|
||||
|
||||
No Phase 2-8 work is authorized before the Phase 1 amendment is applied and validated. The Amendment Procedure does not require a second feature spec, but the Constitution itself remains authoritative until Phase 1 changes it.
|
||||
|
||||
### SemVer handling
|
||||
|
||||
The implementation MUST not predetermine PATCH. Replacing the universal dev rule is a semantic governance change. Phase 1 must select and justify the version under the current policy; a backward-incompatible interpretation requires MAJOR, while any MINOR decision requires an explicit rationale explaining why the change is an expansion rather than an incompatible redefinition.
|
||||
|
||||
### Amendment boundary
|
||||
|
||||
Allowed:
|
||||
|
||||
- replace only the universal dev branch rule;
|
||||
- define dev, platform-dev, and website-dev roles;
|
||||
- define scope-specific feature base, normal target, and diff baseline;
|
||||
- require explicit cross-stream/promotion classification;
|
||||
- forbid silent wrong-family and HEAD~1 fallback;
|
||||
- update required amendment metadata;
|
||||
- identify directly conflicting authoritative artifacts for Phase 2.
|
||||
|
||||
Forbidden:
|
||||
|
||||
- general Constitution cleanup or reduction;
|
||||
- UI/UX, security, architecture, provider, RBAC, OperationRun, or product-governance changes;
|
||||
- skill reorganization;
|
||||
- unrelated guidelines/docs cleanup;
|
||||
- runtime, deployment, or remote-administration changes.
|
||||
|
||||
### Amendment-first close-out evidence
|
||||
|
||||
The Phase 1 hard execution gate remains unchanged. Close-out proof does not
|
||||
attempt to reconstruct wall-clock mutation order from the final diff. It uses
|
||||
the final Constitution amendment, exact boundary and unrelated-scope controls,
|
||||
synchronized dependent instructions and implementation, task/report process
|
||||
attestation, and the absence of a conflicting final state.
|
||||
|
||||
No commit, tag, patch, or snapshot checkpoint was mandated before Spec 439
|
||||
implementation began; none is retroactively claimed. Any future plan requiring
|
||||
independently reconstructable chronology must define and capture that
|
||||
checkpoint before its first mutation.
|
||||
|
||||
## Development Stream and Merge Strategy
|
||||
|
||||
- **Work classification**: repository-governance.
|
||||
- **Feature base**: platform-dev.
|
||||
- **Feature branch**: feat/439-branch-topology-local-evidence-truth.
|
||||
- **Normal merge target**: platform-dev.
|
||||
- **Promotion to dev**: later explicit integration work, outside Spec 439.
|
||||
- **Website convergence**: no delivery contract is created; website family behavior is proven through deterministic fixtures only.
|
||||
- **Constitution amendment delivery**: included in the same feature PR and applied before dependent implementation on the feature branch.
|
||||
|
||||
This strategy does not rename, synchronize, merge, or rebase long-lived branches.
|
||||
|
||||
## Canonical Branch Topology
|
||||
|
||||
| Branch | Role | Ordinary Feature Base | Normal Target | Diff Baseline |
|
||||
|---|---|---|---|---|
|
||||
| dev | Repository-wide integration and promotion | explicit only | explicit promotion/integration | explicit |
|
||||
| platform-dev | TenantPilot platform integration | platform-dev | platform-dev | platform-dev |
|
||||
| website-dev | Website integration | website-dev | website-dev | website-dev |
|
||||
|
||||
Repository-governance work is explicitly classified by affected scope. Spec 439 is platform-owned repository governance and therefore uses platform-dev for base, target, and baseline. Cross-stream and promotion work must declare a target and may target dev. The existence of dev never implies fallback.
|
||||
|
||||
## Unified Resolver Contract
|
||||
|
||||
The resolver makes two separate decisions. This section intentionally matches spec.md verbatim in substance and priority.
|
||||
|
||||
### Stage 1 - Resolve the branch family
|
||||
|
||||
Priority:
|
||||
|
||||
1. explicit CLI or workflow branch-family input;
|
||||
2. pull-request target branch from event context;
|
||||
3. active spec metadata or explicit spec classification;
|
||||
4. current feature-branch naming plus repository-scope signals;
|
||||
5. otherwise fail closed.
|
||||
|
||||
Rules:
|
||||
|
||||
- Explicit input MUST be validated against a known branch family.
|
||||
- A present PR target is authoritative when it maps to a known branch role.
|
||||
- Spec metadata MUST NOT contradict a valid PR target.
|
||||
- Branch-name and path/scope signals are subordinate detection aids and are never sufficient when ambiguous.
|
||||
- The highest-priority usable signal selects the candidate family, but every other present authoritative signal MUST be checked for conflict.
|
||||
- A real conflict, including explicit family platform with PR target website-dev, MUST fail with an actionable conflict diagnostic.
|
||||
- No source may silently override a contradictory authoritative source.
|
||||
- There is no silent default to dev.
|
||||
|
||||
### Stage 2 - Resolve the concrete Git reference
|
||||
|
||||
After the branch family is known:
|
||||
|
||||
1. explicit validated Git ref for the selected branch family;
|
||||
2. matching remote ref, for example origin/platform-dev;
|
||||
3. matching local ref, for example platform-dev;
|
||||
4. validate ancestry and compute the merge base;
|
||||
5. otherwise fail closed.
|
||||
|
||||
Rules:
|
||||
|
||||
- Only refs belonging to the selected branch family are valid.
|
||||
- Missing origin/platform-dev MUST NOT fall back to origin/dev.
|
||||
- Missing origin/website-dev MUST NOT fall back to origin/dev.
|
||||
- HEAD~1 is not a valid integration-baseline fallback.
|
||||
- An explicit cross-stream/promotion target may use dev only after Stage 1 classified the work accordingly.
|
||||
- Output MUST include branch family, expected integration branch, selected ref, target commit, merge-base commit, and resulting base range.
|
||||
- Failure output MUST include the conflicting or attempted signals/refs, explicit override guidance, and confirmation that no diff ran.
|
||||
|
||||
### Terminology
|
||||
|
||||
- **Branch family**: The workstream/integration role selected in Stage 1.
|
||||
- **Concrete Git ref**: The validated remote, local, or explicit ref selected in Stage 2.
|
||||
- **Merge base**: The common commit used to derive feature-relative changes.
|
||||
- **Feature base**: The branch from which ordinary work in a family starts.
|
||||
- **Normal PR target**: The default integration target for ordinary work in a family.
|
||||
- **Promotion target**: An explicitly declared cross-stream/repository integration target, which may be dev.
|
||||
|
||||
## Proposed Repository Changes
|
||||
|
||||
### 1. Narrow Constitution branch-topology amendment
|
||||
|
||||
Update .specify/memory/constitution.md only as authorized by FR-001 through FR-007:
|
||||
|
||||
- replace the universal dev statement;
|
||||
- define all three branch roles and scope-specific routing;
|
||||
- require explicit cross-stream/promotion classification;
|
||||
- forbid silent wrong-family and HEAD~1 fallback;
|
||||
- update the Sync Impact Report, SemVer version, and Last Amended;
|
||||
- include rationale and impacted templates/specs/instructions.
|
||||
|
||||
Validate the amendment diff before any other file changes.
|
||||
|
||||
### 2. Direct authoritative instruction synchronization
|
||||
|
||||
After Phase 1 validation:
|
||||
|
||||
- update only directly conflicting branch instructions in Agents.md;
|
||||
- inspect branch-related authoritative skills/instructions and change only confirmed contradictions;
|
||||
- retain valid platform-dev and promotion guidance;
|
||||
- record unrelated drift as residual debt;
|
||||
- do not rewrite historical completed/deferred specs.
|
||||
|
||||
### 3. Small two-stage branch resolver
|
||||
|
||||
Add one small root executable, expected at scripts/resolve-feature-base unless Phase 0 proves an existing narrower shared path.
|
||||
|
||||
The resolver owns only:
|
||||
|
||||
- fixed branch families and current roles;
|
||||
- Stage 1 signal validation/conflict detection;
|
||||
- Stage 2 family-matching ref resolution;
|
||||
- ancestry and merge-base calculation;
|
||||
- fail-closed diagnostics;
|
||||
- stable human-readable output plus the smallest machine-readable output needed by existing consumers.
|
||||
|
||||
It does not fetch, mutate branches, inspect arbitrary repositories, manage releases, or model user-configurable graphs.
|
||||
|
||||
### 4. Existing changed-file guard integration
|
||||
|
||||
Update scripts/check-ui-productization-coverage to consume the unified resolver while preserving:
|
||||
|
||||
- its explicit base argument;
|
||||
- committed/staged/unstaged/untracked collection;
|
||||
- existing productization checks;
|
||||
- exit 2 before diff when family/ref resolution fails.
|
||||
|
||||
The guard must print family, ref, target, merge base, and base range. No website productization framework is introduced.
|
||||
|
||||
### 5. Existing repository-side workflow declaration coherence
|
||||
|
||||
Inspect .gitea/workflows/test-pr-fast-feedback.yml and contract tests.
|
||||
|
||||
If current event syntax is repo-locally provable, pass the actual PR target through the unified family/ref contract instead of hard-coded origin/dev. Preserve all triggers, jobs, lanes, artifacts, and schedules.
|
||||
|
||||
If syntax is not repo-locally provable, leave the workflow unchanged, record the residual, and do not claim remote validation.
|
||||
|
||||
TestLaneManifest::MAINLINE_BRANCH = dev and main-confidence dev push remain valid promotion semantics.
|
||||
|
||||
### 6. Test execution and report-result preservation
|
||||
|
||||
Extend the existing TestLaneManifest/TestLaneReport path:
|
||||
|
||||
- persist the real test process result during finalizeLane();
|
||||
- carry it through renderLatestReport();
|
||||
- derive one final result from execution, required evidence, evidence consistency, report generation, and blocking budget truth;
|
||||
- keep advisory warnings non-blocking;
|
||||
- preserve both test and report-generation failure contexts;
|
||||
- extend the existing schema minimally.
|
||||
|
||||
### 7. JUnit validity and contradiction detection
|
||||
|
||||
Extend the smallest existing helper to expose:
|
||||
|
||||
- existence and parse validity;
|
||||
- test, failure, error, and skipped counts;
|
||||
- derived evidence status;
|
||||
- affected evidence path in diagnostics.
|
||||
|
||||
Missing/malformed evidence, JUnit failure with passed claim, and failed process with apparently successful evidence cannot normalize to PASS.
|
||||
|
||||
### 8. Blocking versus advisory budgets
|
||||
|
||||
Keep TestLaneBudget::enforcementProfiles() authoritative:
|
||||
|
||||
- hard-fail plus over-budget returns non-zero;
|
||||
- hard-fail within-tolerance warning is non-blocking absent another failure;
|
||||
- soft-warn remains visible and non-blocking;
|
||||
- trend-only remains informational;
|
||||
- missing data required by an active blocking profile fails closed.
|
||||
|
||||
No threshold or profile classification changes are authorized merely to regain green output.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
### Readiness gate
|
||||
|
||||
- **Amendment Procedure**: PASS for implementation readiness. Spec 439 authorizes an explicit Phase 1 update with required rationale, impact list, SemVer, and Last Amended handling.
|
||||
- **Current universal dev rule**: Known contradiction to be replaced by the first mutation; it does not authorize dependent work before Phase 1 validation.
|
||||
- **PROP-001 / ABSTR-001 / BLOAT-001**: PASS. One fixed resolver and one narrow rule replacement are justified by current platform/website and evidence consumers; no general framework.
|
||||
- **SPEC-DISC-001 / LAYER-001**: PASS. Governance, branch truth, and evidence truth remain one coherent repair rather than multiple micro-specs or parallel report layers.
|
||||
- **TEST-TRUTH-001 / TEST-GOV-001**: PASS by design. Unit and bounded Feature tests protect actual outcomes; Fast Feedback and Confidence are affected lanes; no heavy family.
|
||||
- **PERSIST-001 / STATE-001**: N/A for product persistence/state.
|
||||
- **PSC-001 / UI-COV-001**: N/A - no rendered UI.
|
||||
- **RBAC / workspace / provider / OperationRun / customer output**: N/A.
|
||||
|
||||
### Phase gate
|
||||
|
||||
The plan is ready to start Phase 0 and Phase 1. It is not permission to start Phase 2-8 early. Any dependent diff before recorded Phase 1 validation is a plan violation and must be removed or the implementation stopped.
|
||||
|
||||
## Product and Runtime Impact
|
||||
|
||||
- **Platform runtime**: none.
|
||||
- **Website runtime**: none.
|
||||
- **Database/migrations**: none.
|
||||
- **Routes/UI/Filament/Livewire**: none.
|
||||
- **RBAC/policies/capabilities**: none.
|
||||
- **OperationRun/audit/product evidence**: none.
|
||||
- **Customer output**: none.
|
||||
- **Assets**: none; filament:assets is not required.
|
||||
- **Deployment**: no env, migration, queue, scheduler, storage, proxy, or Dokploy change.
|
||||
|
||||
## Project Structure
|
||||
|
||||
Expected implementation surfaces, subject to Phase 0 confirmation:
|
||||
|
||||
.specify/memory/constitution.md
|
||||
Agents.md
|
||||
scripts/
|
||||
├── resolve-feature-base
|
||||
├── check-ui-productization-coverage
|
||||
├── platform-test-lane # only if propagation requires it
|
||||
└── platform-test-report # only if propagation requires it
|
||||
.gitea/workflows/
|
||||
└── test-pr-fast-feedback.yml # conditional existing-target correction
|
||||
apps/platform/
|
||||
├── composer.json # only if compatible entry arguments require it
|
||||
└── tests/
|
||||
├── Support/
|
||||
│ ├── TestLaneManifest.php
|
||||
│ ├── TestLaneReport.php
|
||||
│ └── TestLaneBudget.php # authoritative profiles; expected unchanged
|
||||
├── Unit/Support/
|
||||
│ ├── TestLaneReportTest.php
|
||||
│ └── TestLaneBudgetTest.php
|
||||
└── Feature/Guards/
|
||||
├── BranchTopologyContractTest.php
|
||||
├── ConstitutionBranchTopologyContractTest.php
|
||||
├── TestLaneCommandContractTest.php
|
||||
├── TestLaneArtifactsContractTest.php
|
||||
├── CiLaneFailureClassificationContractTest.php
|
||||
└── directly affected Ci workflow contract tests
|
||||
specs/439-branch-topology-local-evidence-truth/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
├── checklists/requirements.md
|
||||
├── evidence/baseline-attribution.json # compact paired close-out evidence
|
||||
└── implementation-report.md # created only during Phase 8
|
||||
|
||||
Forbidden runtime paths include apps/platform/app, apps/platform/database, apps/platform/routes, apps/platform/resources, and apps/website/src unless the active spec is corrected before any such change.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### A. Constitution and instruction contracts
|
||||
|
||||
Required cases:
|
||||
|
||||
1. branch roles and scope routing are present;
|
||||
2. universal dev feature-base wording is absent from authoritative current instructions;
|
||||
3. cross-stream/promotion requires explicit target;
|
||||
4. wrong-family and HEAD~1 fallback are forbidden;
|
||||
5. unrelated Constitution principles remain unchanged;
|
||||
6. Sync Impact, rationale, SemVer, and Last Amended are coherent.
|
||||
|
||||
### B. Branch-topology fixtures
|
||||
|
||||
Use Pest with Symfony Process and isolated temporary Git repositories. Each fixture configures identity, commits, branches, and refs without network.
|
||||
|
||||
Required cases:
|
||||
|
||||
1. platform remote-ref priority;
|
||||
2. platform local-ref fallback;
|
||||
3. website remote-ref priority;
|
||||
4. website local-ref fallback;
|
||||
5. explicit promotion to dev;
|
||||
6. missing platform refs fail closed;
|
||||
7. missing website refs fail closed;
|
||||
8. diverged platform topology returns only the feature path;
|
||||
9. diverged website topology returns only the feature path;
|
||||
10. explicit platform family plus PR target website-dev fails;
|
||||
11. ambiguous name/path/spec signals fail;
|
||||
12. explicit wrong-family ref is rejected;
|
||||
13. success/failure diagnostics contain required fields.
|
||||
|
||||
### C. Evidence-truth fixtures
|
||||
|
||||
Required cases:
|
||||
|
||||
1. failed process remains failed after refresh;
|
||||
2. passed process remains passed;
|
||||
3. blocking fast-feedback pull-request budget breach returns non-zero;
|
||||
4. advisory confidence breach remains warning and zero absent another failure;
|
||||
5. JUnit failures/errors plus passed claim is contradiction/non-zero;
|
||||
6. missing required JUnit is non-zero;
|
||||
7. malformed required JUnit is non-zero;
|
||||
8. test failure plus report-generation failure preserves both contexts;
|
||||
9. missing required report artifacts remain publication failure;
|
||||
10. existing successful consumers remain readable.
|
||||
|
||||
### D. Focused regression set
|
||||
|
||||
At minimum:
|
||||
|
||||
- new Constitution/branch-topology contract tests;
|
||||
- tests/Unit/Support/TestLaneReportTest.php;
|
||||
- tests/Unit/Support/TestLaneBudgetTest.php;
|
||||
- tests/Feature/Guards/CiLaneFailureClassificationContractTest.php;
|
||||
- tests/Feature/Guards/TestLaneCommandContractTest.php;
|
||||
- tests/Feature/Guards/TestLaneArtifactsContractTest.php;
|
||||
- tests/Feature/Guards/TestLaneManifestTest.php;
|
||||
- directly affected workflow contract tests;
|
||||
- fast-feedback and confidence lanes;
|
||||
- Sail Pint on dirty PHP files;
|
||||
- git diff --check;
|
||||
- forbidden-path and Spec 438 unchanged checks.
|
||||
|
||||
### E. Full-suite decision
|
||||
|
||||
A full product suite is not automatic because no product runtime changes. Fast Feedback and Confidence are required because the shared runner/report path and enforcement profiles change. Browser, PostgreSQL, and Heavy Governance are N/A. Discovering broader cost requires prior spec/plan correction rather than late implicit expansion.
|
||||
|
||||
**Test-governance preparation outcome**: document-in-feature. The bounded Unit/Feature fixture and lane cost is owned and reported by Spec 439; structural remote-CI or release-governance work remains a follow-up-spec.
|
||||
|
||||
## Mandatory Implementation Phases
|
||||
|
||||
### Phase 0 - Revalidate Spec readiness
|
||||
|
||||
- Recheck branch, HEAD, dirty state, local refs, active package, and no-fetch posture.
|
||||
- Reconfirm Amendment Procedure and absence of a mandatory separate Amendment Spec.
|
||||
- Reproduce or statically trace every named defect and direct consumer.
|
||||
- Protect Specs 437/438 and record baselines.
|
||||
- Stop or reconcile stale assumptions before mutation.
|
||||
|
||||
### Phase 1 - Apply the narrowly scoped Constitution branch-topology amendment
|
||||
|
||||
- Identify the exact obsolete universal dev rule.
|
||||
- Amend only branch roles/routing/fallback rules plus required metadata.
|
||||
- Choose and justify SemVer under current policy; never assume PATCH.
|
||||
- Update Sync Impact, rationale, impacted artifacts, and Last Amended.
|
||||
- Validate the amendment diff and record the Phase 1 gate.
|
||||
|
||||
**Hard Gate**: No Phase 2-8 file may change before this gate passes.
|
||||
|
||||
### Phase 2 - Synchronize directly conflicting authoritative instructions
|
||||
|
||||
- Align Agents.md and only preflight-confirmed authoritative contradictions.
|
||||
- Retain valid promotion and platform-dev guidance.
|
||||
- Add/update focused contract checks for synchronized branch wording.
|
||||
- Residualize non-authoritative or unrelated drift.
|
||||
|
||||
### Phase 3 - Implement branch-family and concrete-reference resolution
|
||||
|
||||
- Add failing two-stage resolver fixtures first.
|
||||
- Implement fixed Stage 1 family selection/conflict validation.
|
||||
- Implement fixed Stage 2 family-matching ref/merge-base resolution.
|
||||
- Add success and fail-closed diagnostics.
|
||||
|
||||
### Phase 4 - Update local diff guards and affected repository-side workflow declarations
|
||||
|
||||
- Integrate the resolver into the current changed-file guard.
|
||||
- Preserve explicit-base and all working-tree change sources.
|
||||
- Correct the existing PR target declaration only when repo-locally provable.
|
||||
- Keep all existing triggers, lanes, schedules, and deployment behavior unchanged.
|
||||
|
||||
### Phase 5 - Repair local test/report evidence truth
|
||||
|
||||
- Add failing execution/refresh/JUnit/report fixtures first.
|
||||
- Persist original execution truth.
|
||||
- Extend required evidence metadata and contradiction detection.
|
||||
- Preserve dual failure contexts and compatible successful consumers.
|
||||
|
||||
### Phase 6 - Repair blocking/advisory budget truth
|
||||
|
||||
- Add blocking/advisory/missing-input final-exit controls first.
|
||||
- Derive one final result from authoritative profiles and other failure sources.
|
||||
- Keep thresholds and profile ownership unchanged.
|
||||
- Propagate final result through existing wrappers/entry points.
|
||||
|
||||
### Phase 7 - Add negative fixtures and run focused proof
|
||||
|
||||
- Complete BS-001 through BS-012 and EF-001 through EF-007.
|
||||
- Run Constitution/instruction, resolver, guard, report, budget, artifact, manifest, and workflow contracts.
|
||||
- Run Fast Feedback and Confidence.
|
||||
- Run formatter and diff/scope/history guards.
|
||||
- Record actual outputs without normalizing failures.
|
||||
|
||||
### Phase 8 - Produce implementation report and final reconciliation
|
||||
|
||||
- Create implementation-report.md only now.
|
||||
- Reconcile every FR, INV, task, BS/EF fixture, changed file, test, lane, and residual.
|
||||
- Record the paired clean-baseline/feature attribution with exact SHA,
|
||||
environment and selection equivalence, raw exits, JUnit multiset deltas,
|
||||
performance/budget attribution, and artifact checksums.
|
||||
- Record Product Surface/browser/deployment N/A fields.
|
||||
- Record test-governance outcome and final PASS/PASS WITH CONDITIONS/FAIL.
|
||||
|
||||
## Dependencies and Execution Order
|
||||
|
||||
- Phase 0 blocks Phase 1.
|
||||
- Phase 1 blocks every Phase 2-8 task.
|
||||
- Phase 2 blocks resolver/tooling work that depends on canonical instructions.
|
||||
- Phase 3 blocks Phase 4.
|
||||
- Phase 4 blocks Phase 5; no evidence-truth task may be prepared early.
|
||||
- Phase 5 blocks Phase 6; no budget-truth task may be prepared early.
|
||||
- Phase 7 follows the Phase 6 exit gate.
|
||||
- Phase 8 follows all validation.
|
||||
|
||||
## Parallelization Contract
|
||||
|
||||
- No mutation is parallel with Phase 1.
|
||||
- Same-file Constitution, instruction, resolver, TestLaneReport, and TestLaneManifest edits are single-owner.
|
||||
- Branch fixtures are authored only in Phase 3; evidence fixtures start only after the Phase 4 exit gate.
|
||||
- Workflow declaration work waits for resolver stabilization.
|
||||
- Budget tests begin only after the Phase 5 exit gate and may run in parallel only with file-disjoint tasks inside Phase 6.
|
||||
- No task marked parallel may bypass an earlier phase gate or claim evidence before the producing task completes.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
| Risk | Control |
|
||||
|---|---|
|
||||
| Constitution scope expands | Exact authorized rule list, unrelated-principle diff check, Phase 1 gate |
|
||||
| SemVer is minimized incorrectly | Explicit policy decision and rationale; PATCH forbidden by scope |
|
||||
| dev is removed from valid use | Explicit promotion fixture and unchanged main-confidence semantics |
|
||||
| Signals silently override | Cross-check present authoritative signals and fail on conflict |
|
||||
| Generic branch framework | Fixed families/roles only |
|
||||
| Report schema consumer regression | Compatible extension and positive controls |
|
||||
| Blocking budget exposes debt | Preserve thresholds and report actual result |
|
||||
| Refresh hides failure | Persist original execution result and negative refresh fixture |
|
||||
| Missing evidence becomes empty success | Validity/count/path metadata and fail-closed result |
|
||||
| Remote-CI scope creeps | Conditional minimal existing declaration only |
|
||||
| Historical specs change | Baseline and final byte/diff checks |
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| New structure | Why needed | Simpler alternative rejected because |
|
||||
|---|---|---|
|
||||
| One narrow Constitution branch rule | Current universal dev rule contradicts repository stream ownership | Instruction-only workaround would violate current Constitution |
|
||||
| One small two-stage resolver | Family and concrete ref are different decisions with multiple existing consumers | Duplicated constants and branch-name inference preserve contradiction |
|
||||
| Compatible execution/evidence metadata | Refresh must preserve real test/evidence truth | Console-only state cannot survive report refresh |
|
||||
|
||||
No product entity, status family, taxonomy, package, UI framework, or persistence layer is introduced.
|
||||
|
||||
## Rollout and Compatibility
|
||||
|
||||
- Existing branches are not renamed, deleted, synchronized, rebased, or merged by implementation.
|
||||
- Existing explicit guard base argument remains supported when family-consistent.
|
||||
- Existing valid successful reports remain readable.
|
||||
- Existing thresholds remain unchanged.
|
||||
- Existing remote workflows are not activated or broadened.
|
||||
- Platform local review becomes available after merge to platform-dev.
|
||||
- Website behavior is contract proof only; delivery onto website-dev remains separate integration work.
|
||||
|
||||
## Implementation Readiness Assessment - Preparation Snapshot
|
||||
|
||||
- Candidate Gate: PASS.
|
||||
- Spec Readiness Gate: READY.
|
||||
- Implementation Authorization: AUTHORIZED.
|
||||
- Constitution amendment is in scope and first.
|
||||
- The Amendment Procedure does not require a separate preceding feature spec.
|
||||
- Branch roles and two-stage resolver contract are explicit and synchronized with spec.md.
|
||||
- Evidence truth remains local and bounded.
|
||||
- No product or external-system decision blocks implementation.
|
||||
- Phase 2-8 remain conditionally blocked until Phase 1 validation, as intended.
|
||||
|
||||
**Preparation Plan Gate**: READY - begin a separate implementation loop at Phase 0; Phase 1 is the first mandatory mutation.
|
||||
714
specs/439-branch-topology-local-evidence-truth/spec.md
Normal file
714
specs/439-branch-topology-local-evidence-truth/spec.md
Normal file
@ -0,0 +1,714 @@
|
||||
# Feature Specification: Spec 439 - Branch Topology and Local Evidence Truth
|
||||
|
||||
**Feature Branch**: feat/439-branch-topology-local-evidence-truth
|
||||
**Created**: 2026-07-10
|
||||
**Corrected**: 2026-07-11
|
||||
**Status**: Implemented and reconciled - merge-ready locally with an external activation condition
|
||||
**Type**: Engineering Foundation / Local Governance / Evidence Integrity
|
||||
**Priority**: High
|
||||
**Development Stream**: repository-governance with platform-owned implementation base
|
||||
**Feature Base**: platform-dev
|
||||
**Normal Merge Target**: platform-dev
|
||||
**Input**: Direct user-provided corrected Spec 439 candidate and final manual review findings.
|
||||
|
||||
**Lifecycle boundary**: Readiness, authorization, and “next action” statements below record the requirements-preparation snapshot. Current implementation, evidence, and merge status are owned by `tasks.md` and `implementation-report.md`.
|
||||
|
||||
## Preparation Selection Summary
|
||||
|
||||
- **Selected candidate**: Branch Topology and Local Evidence Truth.
|
||||
- **Source**: Direct user-provided candidate. The automatic candidate queue was not used to invent adjacent scope.
|
||||
- **Roadmap relationship**: Bounded remediation for repository-listed CI/configuration status drift and current local review/evidence defects.
|
||||
- **Why selected**: Current repo truth confirms both trust defects: the changed-file guard defaults to origin/dev or HEAD~1, while the lane report refresh can replace the real execution result with success. Blocking budget classification is not yet guaranteed to affect the final command result.
|
||||
- **Close alternatives deferred**: Full Gitea CI activation, branch protection, deployment gates, release/promotion automation, broad skill cleanup, spec metadata reconciliation, and Spec 438 implementation.
|
||||
- **Completed/deferred-spec guardrail**: Spec 437 is completed read-only context. Spec 438 is merged as a deliberately deferred, non-implemented package and MUST remain unchanged.
|
||||
- **Smallest viable slice**: First amend only the obsolete Constitution branch rule, then synchronize directly conflicting authoritative instructions, then implement one fixed branch-family/base resolver and compatible corrections to the existing local lane result/report path with deterministic fixtures.
|
||||
- **Feature description**: Establish scope-specific branch roles and trustworthy local branch/evidence results without changing product runtime or activating new remote governance.
|
||||
|
||||
## Spec Candidate Check
|
||||
|
||||
- **Problem**: Local feature review can use the wrong integration base, and local test reports can contradict the execution that produced them.
|
||||
- **Today's failure**: Platform history between dev and platform-dev can pollute a feature diff; website work lacks equivalent explicit protection; a failed test can be refreshed into a passed report; a blocking budget can remain non-blocking at process level.
|
||||
- **User-visible improvement**: Developers and reviewers see only the current feature's changes and can trust that local PASS means execution, required evidence, and every blocking condition succeeded.
|
||||
- **Smallest enterprise-capable version**: A narrowly scoped Constitution amendment, direct instruction synchronization, fixed mappings for the known branch roles, fail-closed two-stage resolution, persisted test-result truth, and focused deterministic fixtures.
|
||||
- **Explicit non-goals**: No generic branch graph, release orchestrator, general Constitution rewrite, all-spec metadata migration, Gitea branch protection, new CI lane activation, deployment gate, product runtime change, or broad documentation cleanup.
|
||||
- **Permanent complexity imported**: One small branch-resolution seam, a few compatible report/evidence fields, one narrow Constitution rule amendment, and focused repository-governance tests. No model, table, product enum/state family, UI concept, or runtime service.
|
||||
- **Why now**: The defects affect current local review and implementation evidence before deployment or remote merge gates are required.
|
||||
- **Why not local**: The same branch-family truth serves platform and website validation, and the same lane-result truth serves multiple existing report entry points. Duplicated call-site fixes would preserve contradiction risk.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: #2 New Meta-Infrastructure and #4 Foundation wording.
|
||||
- **Defense**: The resolver is limited to fixed current branch roles and existing consumers, introduces no configurable branch graph, and repairs confirmed false evidence rather than preparing hypothetical infrastructure. The Constitution change replaces one obsolete branch rule and does not reopen unrelated principles.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 2 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**.
|
||||
- **Decision**: approve.
|
||||
|
||||
## Candidate and Readiness Decision
|
||||
|
||||
- **Candidate Gate**: PASS.
|
||||
- **Approval Class**: Core Enterprise.
|
||||
- **Spec Readiness Gate**: READY.
|
||||
- **Implementation Posture**: Targeted governance and evidence remediation.
|
||||
- **Implementation Authorization**: AUTHORIZED.
|
||||
- **First authorized mutation**: The narrowly scoped Constitution branch-topology amendment defined by this spec.
|
||||
- **Dependent-work gate**: No instruction, resolver, guard, workflow, test-support, test, report, or budget implementation may begin until the Phase 1 amendment has been applied on the implementation branch and validated against the Constitution amendment procedure.
|
||||
- **Acceptance posture**: The amendment and dependent implementation may be delivered in the same Spec 439 feature PR. The current governance sources require an explicit Constitution update PR, rationale, impacted-artifact list, SemVer decision, and Last Amended update; they do not require a separate preceding feature spec.
|
||||
- **Depends on**: Spec 437 merged into platform-dev.
|
||||
- **Independent from**: Spec 438, which remains deferred, unimplemented, and unchanged.
|
||||
- **Blocks**: Trustworthy local spec review, reliable local lane reports, and later remote continuous-governance work.
|
||||
|
||||
## Constitution Amendment Authorization
|
||||
|
||||
### Governance basis
|
||||
|
||||
The current Constitution's Amendment Procedure allows an explicit PR to update .specify/memory/constitution.md when the PR includes a short rationale, an impacted templates/specs list, a SemVer decision under the existing policy, and an updated Last Amended date. Neither the Constitution, Agents.md, nor current Spec Kit tooling requires a separate preceding feature spec for that amendment.
|
||||
|
||||
Spec 439 therefore authorizes the amendment as its first mandatory implementation step. This authorization does not itself amend the Constitution. The existing rule remains repo truth until Phase 1 changes and validates it.
|
||||
|
||||
### Authorized amendment scope
|
||||
|
||||
The amendment MAY only:
|
||||
|
||||
1. replace the obsolete universal statement that every feature branch starts from dev;
|
||||
2. define dev as repository-wide integration and promotion, platform-dev as the platform integration branch, and website-dev as the website integration branch;
|
||||
3. define scope-specific feature bases, normal targets, and diff baselines;
|
||||
4. require explicit classification and target declaration for cross-stream integration and promotion work;
|
||||
5. forbid silent fallback to an unrelated branch family or HEAD~1;
|
||||
6. authorize synchronization of directly conflicting authoritative instructions after the amendment is validated;
|
||||
7. update the Constitution Sync Impact Report, version, and Last Amended fields required by the existing amendment procedure.
|
||||
|
||||
### Forbidden amendment scope
|
||||
|
||||
The amendment MUST NOT include:
|
||||
|
||||
- a general Constitution reduction or rewrite;
|
||||
- UI/UX rule cleanup;
|
||||
- skill reorganization;
|
||||
- an architecture-principle audit;
|
||||
- security-guideline cleanup;
|
||||
- changes to unrelated Constitution principles;
|
||||
- product runtime, persistence, RBAC, provider, OperationRun, customer-output, or deployment semantics.
|
||||
|
||||
### Mandatory gate sequence
|
||||
|
||||
- Phase 0 is read-only revalidation.
|
||||
- Phase 1 applies and validates the narrow Constitution amendment.
|
||||
- Phases 2 through 8 are blocked until Phase 1 succeeds.
|
||||
- Failure to validate the amendment stops all dependent work; it does not authorize a workaround in instructions, scripts, workflows, or tests.
|
||||
|
||||
### Amendment-first Evidence Sufficiency
|
||||
|
||||
The prospective amendment-first execution order remains mandatory. Close-out of
|
||||
FR-005, INV-001, INV-002, SC-002, and T056 uses this evidence contract:
|
||||
|
||||
1. The final Constitution amendment is present in the repository state.
|
||||
2. Exact contract tests protect the authorized amendment boundary and required
|
||||
amendment metadata.
|
||||
3. Constitution content outside the authorized branch-topology and required
|
||||
amendment-metadata boundary remains unchanged, with the permitted range
|
||||
verified by normalization or hash controls.
|
||||
4. Dependent instructions and implementations agree with the amended
|
||||
Constitution.
|
||||
5. `tasks.md` and `implementation-report.md` attest that Phase 1 validation
|
||||
preceded dependent work.
|
||||
6. No final repository file conflicts with the Constitution effective after
|
||||
Phase 1.
|
||||
7. Independent chronological Git evidence is required only when a commit, tag,
|
||||
patch, or snapshot checkpoint was explicitly required before the first
|
||||
mutation.
|
||||
8. Any future spec requiring independently reconstructable chronology MUST
|
||||
define and capture that checkpoint before the first mutation.
|
||||
|
||||
Spec 439 required no such pre-mutation historical checkpoint. Its close-out
|
||||
MUST NOT claim one retroactively.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop the affected implementation slice and report the finding when:
|
||||
|
||||
- the Constitution amendment cannot satisfy the current Amendment Procedure without broader principle changes;
|
||||
- amendment review reveals a mandatory separate governance process not present during this correction;
|
||||
- the named branch or evidence defect no longer exists on implementation HEAD and the active artifacts have not been reconciled;
|
||||
- branch classification requires an unresolved product/release decision beyond the fixed current topology;
|
||||
- deterministic local proof requires network access, remote mutation, live Gitea administration, or deployment activation;
|
||||
- the work expands into a generic branch-management, release, CI-platform, or governance framework;
|
||||
- Spec 438 or another completed/deferred spec would need modification.
|
||||
|
||||
An independently provable later sub-slice may continue only when all preceding phase gates remain satisfied.
|
||||
|
||||
## Branch and Integration Strategy
|
||||
|
||||
### Canonical branch roles
|
||||
|
||||
| Branch | Role |
|
||||
|---|---|
|
||||
| dev | Repository-wide integration and promotion branch |
|
||||
| platform-dev | Integration branch for TenantPilot platform work |
|
||||
| website-dev | Integration branch for website work |
|
||||
|
||||
### Normal feature routing
|
||||
|
||||
| Workstream | Feature Base | Normal PR Target | Diff Baseline |
|
||||
|---|---|---|---|
|
||||
| Platform | platform-dev | platform-dev | platform-dev |
|
||||
| Website | website-dev | website-dev | website-dev |
|
||||
| Cross-stream / Promotion | explicitly declared | explicitly declared; may be dev | explicitly declared |
|
||||
| Repository governance | explicitly classified by affected scope | explicitly declared | explicitly declared |
|
||||
|
||||
Spec 439 is repository-governance work with a platform-owned implementation base and normal target of platform-dev. Later platform-dev to dev promotion is outside this spec.
|
||||
|
||||
dev may intentionally lag platform-dev or website-dev. Divergence is not itself a defect. The defect is comparing or targeting ordinary stream work against the wrong branch family.
|
||||
|
||||
No repo-backed platform-dev/dev to website-dev convergence contract is created here. Website behavior is mandatory as a deterministic resolver/diff contract, but delivery of the tooling onto website-dev requires separately governed integration work.
|
||||
|
||||
## Unified Resolver Contract
|
||||
|
||||
The resolver makes two separate decisions. Spec, plan, tasks, fixtures, guard integration, and workflow declarations MUST use this exact contract.
|
||||
|
||||
### Stage 1 - Resolve the branch family
|
||||
|
||||
Priority:
|
||||
|
||||
1. explicit CLI or workflow branch-family input;
|
||||
2. pull-request target branch from event context;
|
||||
3. active spec metadata or explicit spec classification;
|
||||
4. current feature-branch naming plus repository-scope signals;
|
||||
5. otherwise fail closed.
|
||||
|
||||
Rules:
|
||||
|
||||
- Explicit input MUST be validated against a known branch family.
|
||||
- A present PR target is authoritative when it maps to a known branch role.
|
||||
- Spec metadata MUST NOT contradict a valid PR target.
|
||||
- Branch-name and path/scope signals are subordinate detection aids and are never sufficient when ambiguous.
|
||||
- The highest-priority usable signal selects the candidate family, but every other present authoritative signal MUST be checked for conflict.
|
||||
- A real conflict, including explicit family platform with PR target website-dev, MUST fail with an actionable conflict diagnostic.
|
||||
- No source may silently override a contradictory authoritative source.
|
||||
- There is no silent default to dev.
|
||||
|
||||
### Stage 2 - Resolve the concrete Git reference
|
||||
|
||||
After the branch family is known:
|
||||
|
||||
1. explicit validated Git ref for the selected branch family;
|
||||
2. matching remote ref, for example origin/platform-dev;
|
||||
3. matching local ref, for example platform-dev;
|
||||
4. validate ancestry and compute the merge base;
|
||||
5. otherwise fail closed.
|
||||
|
||||
Rules:
|
||||
|
||||
- Only refs belonging to the selected branch family are valid.
|
||||
- Missing origin/platform-dev MUST NOT fall back to origin/dev.
|
||||
- Missing origin/website-dev MUST NOT fall back to origin/dev.
|
||||
- HEAD~1 is not a valid integration-baseline fallback.
|
||||
- An explicit cross-stream/promotion target may use dev only after Stage 1 classified the work accordingly.
|
||||
- Output MUST include branch family, expected integration branch, selected ref, target commit, merge-base commit, and resulting base range.
|
||||
- Failure output MUST include the conflicting or attempted signals/refs, explicit override guidance, and confirmation that no diff ran.
|
||||
|
||||
### Terminology
|
||||
|
||||
- **Branch family**: The workstream/integration role selected in Stage 1.
|
||||
- **Concrete Git ref**: The validated remote, local, or explicit ref selected in Stage 2.
|
||||
- **Merge base**: The common commit used to derive feature-relative changes.
|
||||
- **Feature base**: The branch from which ordinary work in a family starts.
|
||||
- **Normal PR target**: The default integration target for ordinary work in a family.
|
||||
- **Promotion target**: An explicitly declared cross-stream/repository integration target, which may be dev.
|
||||
|
||||
## Spec Scope Fields
|
||||
|
||||
- **Scope**: Repository governance and shared engineering evidence only.
|
||||
- **Primary Routes**: N/A - no application routes or pages.
|
||||
- **Data Ownership**: N/A - no workspace-, environment-, provider-, or customer-owned data changes.
|
||||
- **RBAC**: N/A - no authorization behavior changes.
|
||||
- **Workspace / Managed Environment Isolation**: N/A - no application data access.
|
||||
|
||||
## In Scope
|
||||
|
||||
- The narrow Constitution branch-topology amendment defined above.
|
||||
- Synchronization of directly conflicting authoritative branch instructions after the amendment passes.
|
||||
- A fixed two-stage branch-family and concrete-ref resolver.
|
||||
- Integration of that resolver into the existing changed-file guard.
|
||||
- Minimal correction of existing repository-side PR target declarations when repo-locally provable.
|
||||
- Preservation of original test execution truth through report refresh.
|
||||
- Required JUnit/report evidence validation and contradiction detection.
|
||||
- Enforcement of existing blocking/advisory budget classifications in the final local result.
|
||||
- Deterministic local Git/JUnit/report/budget fixtures.
|
||||
- Focused validation and an implementation report.
|
||||
|
||||
## No Legacy / No Backward Compatibility Constraint
|
||||
|
||||
- **Compatibility posture**: Canonical replacement of incorrect branch and result defaults. Structurally valid successful reports remain readable; artifacts lacking newly required execution/evidence truth may be read diagnostically but MUST NOT be accepted as PASS.
|
||||
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: No new compatibility path. Existing historical specs and reports are not rewritten.
|
||||
- **Why clean replacement is safe now**: The change affects repository tooling and engineering evidence in a pre-production repository. Incorrect dev/HEAD~1 fallback and false-success behavior are not valid compatibility contracts.
|
||||
|
||||
## UI Surface Impact
|
||||
|
||||
- [x] No UI surface impact
|
||||
- [ ] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [ ] New modal/drawer/wizard/action added
|
||||
- [ ] New table/form/state added
|
||||
- [ ] Customer-facing surface changed
|
||||
- [ ] Dangerous action changed
|
||||
- [ ] Status/evidence/review presentation changed
|
||||
- [ ] Workspace/environment context presentation changed
|
||||
|
||||
**No-impact rationale**: Only governance documents, local Git/diff tooling, shared test-lane evidence helpers, focused tests, and existing repository-side workflow declarations are in implementation scope. No rendered platform or website surface changes.
|
||||
|
||||
## UI/Productization Coverage
|
||||
|
||||
N/A - no reachable UI surface impact. No route inventory, design coverage, screenshot, page audit, customer-safe review, or dangerous-action review is required.
|
||||
|
||||
## Product Surface Impact
|
||||
|
||||
- **Product Surface Contract applies?**: No - no rendered product surface changes.
|
||||
- **Page archetype**: N/A.
|
||||
- **Primary user question**: N/A.
|
||||
- **Primary action**: N/A.
|
||||
- **Surface budget result**: N/A.
|
||||
- **Technical Annex / deep-link demotion**: N/A.
|
||||
- **Canonical status vocabulary**: N/A - engineering report statuses are not product status vocabulary.
|
||||
- **Visible complexity impact**: N/A.
|
||||
- **Product Surface exceptions**: none.
|
||||
|
||||
## Browser Verification Plan
|
||||
|
||||
- **Browser proof required?**: no.
|
||||
- **No-browser rationale**: N/A - no rendered UI surface changed.
|
||||
- **Focused path / interaction**: N/A.
|
||||
- **Console, Livewire, Filament, network, and 500-error checks**: N/A.
|
||||
- **Conditional browser lane**: Not authorized by this spec. Unexpected rendered-surface scope requires a prior spec/plan correction.
|
||||
|
||||
## Human Product Sanity Check
|
||||
|
||||
- **Required?**: no.
|
||||
- **No-human-sanity rationale**: N/A - no product surface changed.
|
||||
- **Planned result location**: Implementation report records the no-surface decision and neutral visible-complexity outcome.
|
||||
|
||||
## Product Surface Merge Gate Checklist
|
||||
|
||||
- [x] No-legacy posture recorded.
|
||||
- [x] Product Surface Impact is N/A with rationale.
|
||||
- [x] Browser proof is N/A - no rendered UI surface changed.
|
||||
- [x] Human Product Sanity is not applicable with rationale.
|
||||
- [x] Product Surface exceptions are none.
|
||||
- [x] Implementation report must state Livewire v4 compliance, provider registration location, global-search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible-complexity outcome, and completed-spec rewrite safety.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse
|
||||
|
||||
- **Cross-cutting feature?**: yes, limited to repository branch resolution and shared engineering test evidence.
|
||||
- **Systems touched**: Constitution branch instruction, directly conflicting authoritative instructions, root changed-file guard, existing test-lane manifest/budget/report path, focused governance tests, and existing PR workflow declaration only when needed to remove a hard-coded target assumption.
|
||||
- **Existing patterns to extend**: git merge-base, TestLaneManifest, TestLaneBudget, TestLaneReport, and existing workflow contract tests.
|
||||
- **New abstraction**: At most one small fixed branch resolver or equivalent canonical mapping. No registry, configurable graph, or release framework.
|
||||
- **Allowed deviation**: none.
|
||||
- **Review focus**: One branch-family/ref contract and one final-result contract remain authoritative; no duplicated budget classification or hidden dev fallback.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
N/A - no OperationRun creation, lifecycle, link, notification, or evidence semantics are touched.
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
N/A - no provider/platform product boundary is touched.
|
||||
|
||||
## UI / Surface Guardrail Impact
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / N/A Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Repository tooling and engineering test reports | no | N/A | none | none | no | N/A - repository workflow only |
|
||||
|
||||
## Decision-First Surface Role
|
||||
|
||||
N/A - no operator-facing surface changed.
|
||||
|
||||
## Audience-Aware Disclosure
|
||||
|
||||
N/A - no customer, operator, support, report, review-pack, or raw-evidence product surface changed.
|
||||
|
||||
## UI/UX Surface Classification
|
||||
|
||||
N/A - no operator-facing surface changed.
|
||||
|
||||
## Operator Surface Contract
|
||||
|
||||
N/A - no operator-facing surface changed.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **New source of truth?**: No product source of truth. A small repository mapping becomes canonical only for engineering branch resolution.
|
||||
- **New persisted entity/table/artifact?**: no.
|
||||
- **New abstraction?**: yes, at most one small fixed branch-family/base resolver or equivalent mapping.
|
||||
- **New enum/state/reason family?**: no product enum/state family. Work classifications remain command/spec metadata only.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: Developers/reviewers can evaluate unrelated history and accept false local PASS evidence.
|
||||
- **Existing structure is insufficient because**: Branch defaults are duplicated or hard-coded, and execution/report refresh do not carry one authoritative final result through existing entry points.
|
||||
- **Narrowest correct implementation**: Three fixed branch roles, a small two-stage resolver, one narrow Constitution rule replacement, and compatible final-result/evidence checks in existing report classes.
|
||||
- **Ownership cost**: A focused Constitution contract, shell/Pest fixtures, and maintenance when the repository intentionally changes branch topology or lane semantics.
|
||||
- **Alternative intentionally rejected**: Duplicated branch constants, branch-name-only inference, a general branch graph, report-schema replacement, a separate governance framework, or remote merge-governance activation.
|
||||
- **Release truth**: Current repository workflow safety, not future-release preparation.
|
||||
|
||||
## Testing / Lane / Runtime Impact
|
||||
|
||||
- **Test purpose / classification**: Feature for Constitution/instruction/branch/wrapper/workflow contracts; Unit for JUnit/result/budget consistency. No new Heavy-Governance family.
|
||||
- **Validation lanes**: Focused files first, then affected fast-feedback and confidence lanes. Browser, PostgreSQL, and Heavy Governance are N/A.
|
||||
- **Why sufficient**: No database, Laravel product runtime, Filament, Livewire, provider, workspace, session, or browser behavior is involved. Deterministic filesystem/process fixtures prove the trust contract directly.
|
||||
- **New or expanded test families**: One bounded branch-topology Feature contract plus focused additions to existing Unit/Feature evidence-truth contracts.
|
||||
- **Fixture/helper cost impact**: Temporary local Git repositories and small JUnit/report fixtures; no database/factory/seed/provider/workspace defaults.
|
||||
- **Heavy-family visibility**: none.
|
||||
- **Budget/baseline/trend impact**: Existing Fast Feedback and Confidence thresholds remain unchanged. Correct enforcement may expose existing blocking debt; thresholds MUST NOT be relaxed to regain green output.
|
||||
- **Escalation**: document-in-feature for contained lane-result changes; follow-up-spec only for structural remote-CI or release-governance work.
|
||||
- **Reviewer handoff**: Verify amendment scope, phase gating, all branch conflict/fallback fixtures, failed/pass report controls, blocking/advisory controls, missing/malformed evidence, and JUnit/report contradiction cases.
|
||||
- **Test-governance preparation outcome**: document-in-feature.
|
||||
- **Planned validation commands**: Exact focused commands are defined in plan.md and tasks.md; no runtime/browser proof is required.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
TenantPilot has three long-lived development branches with different responsibilities. The current universal dev rule contradicts actual stream ownership and must be replaced before dependent tooling can claim the new topology.
|
||||
|
||||
Engineering evidence is trustworthy only when the test process result, required JUnit evidence, generated report, budget enforcement, and final command result agree. Current refresh behavior can replace the real result with success and can leave blocking classifications non-blocking at process level.
|
||||
|
||||
## Business Value and Primary Users
|
||||
|
||||
- **Primary users**: Platform developers, website developers, spec reviewers, governance maintainers, and test-governance maintainers.
|
||||
- **Business/trust value**: Feature scope and implementation evidence become reliable inputs to merge/readiness decisions.
|
||||
- **Before**: Reviewers can see unrelated stream history or a passed refreshed report after failed execution.
|
||||
- **After**: Reviewers see the feature-relative diff and a PASS only when execution, required evidence, and every blocking condition agree.
|
||||
|
||||
## Governing Invariants
|
||||
|
||||
- **INV-001 Amendment first**: The branch-topology Constitution amendment is the first mutating implementation step. Close-out evidence follows Amendment-first Evidence Sufficiency and does not imply independent historical chronology without a pre-required checkpoint.
|
||||
- **INV-002 Dependent-work gate**: Phases 2-8 cannot begin until Phase 1 is applied and validated. Tasks and the implementation report attest the gate, while final repository state and exact boundary controls corroborate it.
|
||||
- **INV-003 Narrow amendment**: No unrelated Constitution principle may change.
|
||||
- **INV-004 Scope-specific integration**: There is no universal integration branch for every repository change.
|
||||
- **INV-005 Platform target**: Ordinary platform features use platform-dev for base, target, and diff baseline.
|
||||
- **INV-006 Website target**: Ordinary website features use website-dev for base, target, and diff baseline.
|
||||
- **INV-007 Valid dev role**: dev remains valid for explicitly classified repository integration and promotion.
|
||||
- **INV-008 No stream-to-dev fallback**: Missing platform/website refs never select dev, origin/dev, HEAD~1, or an arbitrary commit.
|
||||
- **INV-009 Explicit classification**: Cross-stream and promotion work require declared family and target.
|
||||
- **INV-010 Two-stage resolution**: Branch family is resolved before a concrete Git ref.
|
||||
- **INV-011 Conflict is failure**: Contradictory authoritative signals fail with actionable diagnostics.
|
||||
- **INV-012 Merge-base diff**: Feature-relative paths use the merge base of HEAD and the selected ref.
|
||||
- **INV-013 Test failure immutability**: Later reporting cannot turn a non-zero test execution into PASS.
|
||||
- **INV-014 Blocking means blocking**: A blocking budget affects the final command result.
|
||||
- **INV-015 Advisory means advisory**: Advisory thresholds warn without failing solely for the warning.
|
||||
- **INV-016 Evidence agreement**: Process, JUnit, report, budget, and final exit cannot materially contradict one another.
|
||||
- **INV-017 Missing required evidence is not success**: Missing or malformed required evidence fails closed.
|
||||
- **INV-018 No product runtime change**: No platform or website customer behavior changes.
|
||||
- **INV-019 Historical-spec safety**: Specs 437 and 438 remain read-only.
|
||||
- **INV-020 Local-only proof**: Required proof is deterministic and network-free.
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - Governance Maintainer Establishes the Branch Contract (Priority: P1)
|
||||
|
||||
As a governance maintainer, I need the obsolete universal dev rule replaced through the existing amendment procedure before dependent tooling adopts scope-specific branches.
|
||||
|
||||
**Independent Test**: The Constitution diff changes only the branch-topology rule and required Sync Impact/version/date metadata; it defines all three roles, records rationale/impacts, and leaves unrelated principles byte-for-byte unchanged.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- The amendment is inside Spec 439 scope and is the first mutating step.
|
||||
- The amendment defines dev, platform-dev, and website-dev roles.
|
||||
- The amendment records rationale, impacted artifacts, SemVer decision, and Last Amended.
|
||||
- Directly conflicting authoritative instructions are synchronized only after amendment validation.
|
||||
- No unrelated Constitution or guideline cleanup occurs.
|
||||
|
||||
### User Story 2 - Stream Developers See the Real Feature Diff (Priority: P1)
|
||||
|
||||
As a platform or website developer, I need validation to use my stream's integration family so unrelated history is excluded.
|
||||
|
||||
**Independent Test**: Diverged temporary repositories for platform and website return only the feature path for the selected family.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- Platform resolves to origin/platform-dev or local platform-dev.
|
||||
- Website resolves to origin/website-dev or local website-dev.
|
||||
- Missing family refs fail with attempted-ref guidance and no diff.
|
||||
- Neither family silently selects dev, origin/dev, or HEAD~1.
|
||||
|
||||
### User Story 3 - Cross-Stream and Promotion Work Are Deliberate (Priority: P1)
|
||||
|
||||
As a reviewer, I need conflicting signals to fail and promotion work to use dev only when explicitly classified.
|
||||
|
||||
**Independent Test**: Explicit platform plus PR target website-dev fails, while explicit promotion to dev succeeds.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- Branch-family and ref resolution remain separate.
|
||||
- Conflicting authoritative signals fail.
|
||||
- A valid ref must belong to the selected family.
|
||||
- Explicit promotion to dev remains valid.
|
||||
|
||||
### User Story 4 - Failed Tests Cannot Look Successful (Priority: P1)
|
||||
|
||||
As a reviewer, I need report refresh to preserve the real test result.
|
||||
|
||||
**Independent Test**: A non-zero test fixture followed by refresh remains failed in report and final exit; a passing positive control remains passed.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- Original execution result is persisted.
|
||||
- Refresh never hard-codes success.
|
||||
- Test failure remains visible when report generation also fails.
|
||||
- Existing valid successful report consumers remain readable.
|
||||
|
||||
### User Story 5 - Budget and Required Evidence Truth Are Enforced (Priority: P1)
|
||||
|
||||
As a test-governance maintainer, I need blocking budgets and required evidence to fail while advisory budgets remain warnings.
|
||||
|
||||
**Independent Test**: Blocking, advisory, contradiction, missing, malformed, and dual-failure fixtures produce their specified statuses and exits.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- Blocking breach returns non-zero without threshold relaxation.
|
||||
- Advisory-only breach remains visible and non-blocking.
|
||||
- Missing/malformed evidence and contradictions return non-zero.
|
||||
- Execution, evidence, budget, report, and final-result states remain distinguishable.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Constitution Amendment and Phase Gate
|
||||
|
||||
- **FR-001**: Spec 439 MUST authorize the narrow branch-topology Constitution amendment as its first mutating implementation step.
|
||||
- **FR-002**: The amendment MUST follow the existing Amendment Procedure, including rationale, impacted-artifact list, SemVer decision, and updated Last Amended date.
|
||||
- **FR-003**: The amendment MUST define the three branch roles and scope-specific feature routing in this spec.
|
||||
- **FR-004**: The amendment MUST NOT change unrelated Constitution principles or initiate general governance cleanup.
|
||||
- **FR-005**: No Phase 2-8 work may begin until the Phase 1 amendment is applied and validated. Close-out evidence for this ordering MUST satisfy Amendment-first Evidence Sufficiency and MUST NOT require or claim independent historical chronology unless a pre-mutation checkpoint was explicitly required.
|
||||
- **FR-006**: Directly conflicting authoritative instructions MUST be synchronized after, not before, amendment validation.
|
||||
- **FR-007**: The same feature PR MAY carry the amendment and dependent work; no separate preceding Amendment Spec is required by current repo governance.
|
||||
|
||||
### Branch Roles and Routing
|
||||
|
||||
- **FR-008**: dev MUST mean repository-wide integration and promotion.
|
||||
- **FR-009**: platform-dev MUST be the ordinary platform feature base, normal PR target, and diff baseline.
|
||||
- **FR-010**: website-dev MUST be the ordinary website feature base, normal PR target, and diff baseline.
|
||||
- **FR-011**: Cross-stream, promotion, and repository-governance work MUST declare family, target, and baseline when no unambiguous fixed rule exists.
|
||||
- **FR-012**: dev MUST NOT be treated as obsolete or as a silent default.
|
||||
- **FR-013**: Divergence between long-lived branches MUST NOT be classified as a defect by itself.
|
||||
|
||||
### Stage 1 - Branch-Family Resolution
|
||||
|
||||
- **FR-014**: Stage 1 MUST use this priority: explicit family input, PR target, active spec metadata/classification, feature-name plus repo-scope signals, then fail closed.
|
||||
- **FR-015**: Explicit family input MUST be validated.
|
||||
- **FR-016**: A known PR target MUST be authoritative and MUST be checked against other authoritative signals.
|
||||
- **FR-017**: Spec metadata MUST NOT contradict a valid PR target.
|
||||
- **FR-018**: Feature-branch names and repository paths MUST be subordinate hints only.
|
||||
- **FR-019**: Ambiguous or conflicting signals MUST fail with actionable diagnostics.
|
||||
- **FR-020**: Stage 1 MUST NOT silently default to dev.
|
||||
|
||||
### Stage 2 - Concrete-Ref and Merge-Base Resolution
|
||||
|
||||
- **FR-021**: Stage 2 MUST run only after Stage 1 selects a branch family.
|
||||
- **FR-022**: Stage 2 MUST use this priority: explicit family-matching ref, matching remote ref, matching local ref, ancestry/merge-base validation, then fail closed.
|
||||
- **FR-023**: Refs from another branch family MUST be rejected.
|
||||
- **FR-024**: Missing platform/website refs MUST NOT fall back to dev, origin/dev, HEAD~1, or an arbitrary commit.
|
||||
- **FR-025**: Explicit promotion may use dev only after Stage 1 classifies it as promotion/cross-stream integration.
|
||||
- **FR-026**: Success output MUST include family, expected branch, selected ref, target commit, merge base, and base range.
|
||||
- **FR-027**: Failure output MUST include conflicts or attempted refs, override guidance, and confirmation that no diff ran.
|
||||
- **FR-028**: Changed paths MUST use merge-base semantics.
|
||||
|
||||
### Guard, Instructions, and Existing Workflow Declarations
|
||||
|
||||
- **FR-029**: The current changed-file guard MUST consume the unified resolver contract and preserve its explicit base argument.
|
||||
- **FR-030**: The guard MUST continue to include committed, staged, unstaged, and untracked active-feature changes.
|
||||
- **FR-031**: Existing event-supplied actual targets MUST remain authoritative when valid and family-consistent.
|
||||
- **FR-032**: Workflow declarations MAY change only to remove a hard-coded wrong target or propagate the same final-result semantics; no new trigger, lane, schedule, merge gate, or deployment behavior is authorized.
|
||||
- **FR-033**: Direct instruction alignment MUST remain limited to confirmed authoritative contradictions.
|
||||
|
||||
### Test Exit, Report, and Evidence Truth
|
||||
|
||||
- **FR-034**: The test-process exit MUST be captured immediately and persisted in the report contract.
|
||||
- **FR-035**: Report refresh MUST derive or receive the stored execution result and MUST NOT hard-code success.
|
||||
- **FR-036**: A failed test MUST never be represented as passed.
|
||||
- **FR-037**: Refresh MAY run after failure but MUST preserve a non-zero final result.
|
||||
- **FR-038**: Dual test/report failure MUST preserve both facts and remain failed.
|
||||
- **FR-039**: JUnit parsing MUST distinguish missing, malformed, passed, failed, error-containing, and skipped evidence.
|
||||
- **FR-040**: Process/JUnit/report contradictions MUST be visible and non-zero.
|
||||
- **FR-041**: Missing or malformed required JUnit/report artifacts MUST NOT produce success.
|
||||
- **FR-042**: Existing valid successful reports SHOULD remain readable through minimal compatible extension.
|
||||
|
||||
### Budget Truth
|
||||
|
||||
- **FR-043**: Existing TestLaneBudget enforcement profiles MUST remain the sole blocking/advisory authority.
|
||||
- **FR-044**: Workflow YAML, wrappers, and Composer MUST NOT duplicate budget classification.
|
||||
- **FR-045**: A blocking budget breach MUST return non-zero from the final lane/report entry point.
|
||||
- **FR-046**: Advisory-only warnings MAY return zero but MUST remain visible.
|
||||
- **FR-047**: Thresholds MUST NOT be relaxed or profiles reclassified merely to obtain green output.
|
||||
- **FR-048**: Missing/malformed inputs required by an active blocking profile MUST fail closed.
|
||||
- **FR-049**: Local and reusable existing CI entry points MUST use the same final-result semantics.
|
||||
|
||||
### Runtime and Scope Discipline
|
||||
|
||||
- **FR-050**: No product model, migration, controller, service, policy, job, provider, Filament, Livewire, route, view, website component, customer output, or product data behavior may change.
|
||||
- **FR-051**: Tests MUST require no network, fetch, live Gitea, PR creation, remote branch mutation, or branch-protection API.
|
||||
- **FR-052**: Spec 438 MUST remain deferred, unimplemented, and unchanged.
|
||||
- **FR-053**: Remote branch protection, deployment gates, and general CI-platform activation MUST remain out of scope.
|
||||
- **FR-054**: The implementation report MUST reconcile every requirement, invariant, fixture, changed file, validation result, and residual.
|
||||
|
||||
## Required Deterministic Fixtures
|
||||
|
||||
| ID | Fixture | Expected result |
|
||||
|---|---|---|
|
||||
| BS-001 | Platform remote ref available | origin/platform-dev selected |
|
||||
| BS-002 | Platform remote ref absent, local ref available | platform-dev selected |
|
||||
| BS-003 | Website remote ref available | origin/website-dev selected |
|
||||
| BS-004 | Website remote ref absent, local ref available | website-dev selected |
|
||||
| BS-005 | Explicit promotion | dev accepted after promotion classification |
|
||||
| BS-006 | Missing platform family refs | fail closed; no fallback diff |
|
||||
| BS-007 | Missing website family refs | fail closed; no fallback diff |
|
||||
| BS-008 | Diverged platform topology | only platform feature path returned |
|
||||
| BS-009 | Diverged website topology | only website feature path returned |
|
||||
| BS-010 | Explicit family platform plus PR target website-dev | actionable conflict failure |
|
||||
| BS-011 | Ambiguous name/path/spec signals | actionable classification failure |
|
||||
| BS-012 | Explicit ref from wrong family | rejected before merge-base calculation |
|
||||
| EF-001 | Failed test then refresh | failed report and non-zero exit |
|
||||
| EF-002 | Passed test | passed report and zero exit |
|
||||
| EF-003 | Blocking budget exceeded | blocking failure and non-zero exit |
|
||||
| EF-004 | Advisory budget exceeded | visible warning and zero exit |
|
||||
| EF-005 | JUnit failure plus passed claim | contradiction and non-zero exit |
|
||||
| EF-006 | Missing/malformed required evidence | evidence failure and non-zero exit |
|
||||
| EF-007 | Test failure plus report failure | both contexts visible; non-zero exit |
|
||||
|
||||
Temporary Git fixtures MUST create their own histories, MUST NOT depend on real divergence counts, and MUST require no network.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- Resolution and evidence outcomes MUST be deterministic from local inputs.
|
||||
- Diagnostic failures MUST be actionable and name conflicting signals, attempted refs, or affected evidence files.
|
||||
- Shell tooling MUST remain portable to the repository's current Bash environment.
|
||||
- Report changes MUST remain compatible for existing valid successful consumers where truth can be preserved.
|
||||
- New tests MUST stay filesystem/process-only and avoid database or product application boot unless an existing narrow convention requires it.
|
||||
- Constitution changes MUST remain reviewable as a narrow branch-rule diff with explicit Sync Impact and SemVer rationale.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- No general Constitution cleanup, shortening, reorganization, or audit.
|
||||
- No UI/UX, security, architecture, provider, RBAC, OperationRun, or product-surface governance changes.
|
||||
- No merge or synchronization among dev, platform-dev, and website-dev.
|
||||
- No branch rename or deletion.
|
||||
- No generic branch graph, release manager, or multi-repository router.
|
||||
- No broad skill or instruction cleanup beyond directly conflicting authoritative branch statements.
|
||||
- No full Gitea administration, branch protection, remote merge blocking, deployment gate, staging activation, or production activation.
|
||||
- No new heavy/browser/PostgreSQL lane activation.
|
||||
- No all-spec metadata migration, historical report reconciliation, Ledger/Roadmap rewrite, or Spec Candidates update.
|
||||
- No implementation of Spec 438.
|
||||
- No product runtime, customer-facing, database, RBAC, provider, OperationRun, audit-domain, or UI changes.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The current Amendment Procedure remains authoritative and does not require a separate preceding feature spec.
|
||||
- platform-dev and website-dev remain the ordinary integration branches for their streams.
|
||||
- dev remains the repository-wide integration/promotion branch.
|
||||
- Existing TestLaneBudget profiles express the intended current blocking/advisory decisions.
|
||||
- Existing lane reports may be minimally extended with execution/evidence metadata.
|
||||
- The current feature branch remains the implementation branch; Phase 0 revalidates HEAD, refs, and dirty state without fetching.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **R-001 Amendment scope creep**: Diff only the branch rule and required Sync Impact/version/date metadata; stop on unrelated principle changes.
|
||||
- **R-002 dev treated as obsolete**: Keep explicit promotion-to-dev fixture and canonical role text.
|
||||
- **R-003 False stream detection**: Cross-check all present authoritative signals; conflicts fail.
|
||||
- **R-004 Generic framework creep**: Fixed branch roles/classifications only.
|
||||
- **R-005 Existing blocking breaches become visible**: Do not weaken thresholds; fix the cause or record a separately justified decision.
|
||||
- **R-006 Report consumer breakage**: Extend the existing schema minimally and retain positive controls.
|
||||
- **R-007 Remote-CI scope creep**: No new triggers/lanes/gates; remote execution remains residual unless a minimal existing declaration correction is repo-locally provable.
|
||||
- **R-008 Historical spec damage**: Verify Spec 437 and Spec 438 unchanged.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-001**: Phase 1 produces one narrow Constitution branch-topology amendment satisfying rationale, impact-list, SemVer, and Last Amended rules.
|
||||
- **SC-002**: The amendment-first sequence required by FR-005 is process-attested in `tasks.md` and `implementation-report.md` and corroborated by the final Constitution state, exact amendment-boundary and unrelated-scope controls, synchronized dependent artifacts, and the absence of a conflicting final state. Independent historical chronology is required only when a pre-mutation checkpoint was specified; Spec 439 specified none and claims none retroactively.
|
||||
- **SC-003**: All twelve BS fixtures pass deterministically without network.
|
||||
- **SC-004**: All seven EF fixtures pass deterministically.
|
||||
- **SC-005**: Platform and website fixtures never silently select dev, origin/dev, or HEAD~1.
|
||||
- **SC-006**: Conflicting authoritative signals and wrong-family refs fail before diff execution.
|
||||
- **SC-007**: Failed tests, missing evidence, contradictions, and blocking budgets return non-zero.
|
||||
- **SC-008**: Advisory-only budgets remain visible and non-blocking.
|
||||
- **SC-009**: Relevant existing lane/report/guard contracts remain green.
|
||||
- **SC-010**: No product-runtime or rendered-surface file changes occur.
|
||||
- **SC-011**: Spec 438 remains byte-for-byte unchanged by the implementation branch.
|
||||
- **SC-012**: The implementation report contains a complete requirement/task/fixture/result reconciliation and a permitted final verdict.
|
||||
|
||||
## Mandatory Implementation Phases
|
||||
|
||||
1. **Phase 0 - Revalidate Spec readiness**: Read-only branch, HEAD, dirty-state, governance-process, defect, and consumer revalidation.
|
||||
2. **Phase 1 - Apply the narrowly scoped Constitution branch-topology amendment**: Amend the obsolete rule, Sync Impact Report, SemVer, and Last Amended only.
|
||||
3. **Phase 2 - Synchronize directly conflicting authoritative instructions**: Align Agents.md and only other confirmed authoritative contradictions.
|
||||
4. **Phase 3 - Implement branch-family and concrete-reference resolution**: Add tests first, then the fixed two-stage resolver.
|
||||
5. **Phase 4 - Update local diff guards and affected repository-side workflow declarations**: Consume the resolver and remove only proven wrong-target assumptions.
|
||||
6. **Phase 5 - Repair local test/report evidence truth**: Preserve execution result and validate JUnit/report consistency.
|
||||
7. **Phase 6 - Repair blocking/advisory budget truth**: Derive final exit from authoritative profiles without changing thresholds.
|
||||
8. **Phase 7 - Add negative fixtures and run focused proof**: Complete fallback/conflict/evidence controls and focused lanes.
|
||||
9. **Phase 8 - Produce implementation report and final reconciliation**: Record all evidence, residuals, scope checks, and final verdict.
|
||||
|
||||
Phases 2-8 MUST NOT begin until Phase 1 has been applied and validated. Phase 0 is the only work permitted before Phase 1.
|
||||
|
||||
## Parallelization Contract
|
||||
|
||||
- Phase 0 and Phase 1 are strictly sequential.
|
||||
- Phase 2 begins only after the Phase 1 gate and remains sequential for shared instruction files.
|
||||
- Within Phase 3, branch fixture authoring precedes resolver implementation; same-file test/resolver work is single-owner.
|
||||
- Phase 4 follows the Phase 3 exit gate.
|
||||
- Phase 5 follows the Phase 4 exit gate, and Phase 6 follows the Phase 5 exit gate; neither phase may prepare work early.
|
||||
- File-disjoint tasks may run in parallel only inside the currently released phase; shared TestLaneReport/TestLaneManifest edits are single-owner.
|
||||
- Phase 7 begins only after Phases 3-6 converge.
|
||||
- Phase 8 is strictly last.
|
||||
- No parallel task may bypass an earlier phase gate, edit the same file concurrently, or claim implementation evidence before its producing task completes.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- The narrow Constitution amendment is applied and validated under the existing Amendment Procedure.
|
||||
- Directly conflicting authoritative instructions use the same branch roles.
|
||||
- Spec, plan, tasks, checklist, resolver, guard, and applicable workflow declaration use the same two-stage resolver contract.
|
||||
- BS-001 through BS-012 and EF-001 through EF-007 pass with recorded expected/actual results.
|
||||
- Original test exit, required evidence, contradictions, report generation, and blocking/advisory budgets determine one truthful final result.
|
||||
- Existing thresholds are unchanged.
|
||||
- No forbidden runtime, UI, deployment, remote-administration, or historical-spec paths changed.
|
||||
- Required focused tests, lanes, formatter, and diff validation are recorded.
|
||||
- The implementation report reconciles requirements, invariants, changed files, fixtures, tests, residuals, and final verdict.
|
||||
|
||||
## Implementation Report Requirements
|
||||
|
||||
The later implementation report MUST record:
|
||||
|
||||
- branch, HEAD, dirty state, active spec, and local refs for dev, platform-dev, and website-dev;
|
||||
- whether any fetch occurred;
|
||||
- the Constitution amendment diff, rationale, impacted artifacts, SemVer decision, Last Amended value, and Phase 1 validation;
|
||||
- amendment-first process attestation and the corroborating final-state evidence required by Amendment-first Evidence Sufficiency, including checkpoint status and an explicit statement that no independent historical chronology is claimed unless it was required before mutation;
|
||||
- paired baseline-attribution evidence containing the exact baseline SHA, environment and lane-selection comparison, raw exits, JUnit multiset deltas, performance/budget attribution, and artifact checksums;
|
||||
- Spec 437/438 protection evidence;
|
||||
- per-file rationale and requirement/invariant traceability;
|
||||
- each BS and EF fixture's expected/actual family/ref/merge-base/status/exit;
|
||||
- test/lane selection and budget/baseline/trend outcomes;
|
||||
- external Gitea/branch-protection/deployment residuals;
|
||||
- test-governance outcome: keep, split, document-in-feature, follow-up-spec, or reject-or-split;
|
||||
- final verdict PASS, PASS WITH CONDITIONS, or FAIL under this spec;
|
||||
- Livewire v4 compliance; provider registration unchanged at bootstrap/providers.php; global-search posture; destructive/high-impact action posture; asset strategy; browser N/A; deployment impact; visible-complexity result; Product Surface exceptions; completed-spec rewrite safety.
|
||||
|
||||
PASS WITH CONDITIONS is permitted only for external remote verification not needed for local correctness. It is forbidden when the amendment gate, branch fixtures, result preservation, blocking budgets, required evidence, contradiction detection, or fail-closed branch-family behavior remain unproved.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
1. Spec Package Metadata and Reconciliation.
|
||||
2. Capability, Skill and Product-Truth Drift Reporting.
|
||||
3. Gitea CI and Merge Evidence Activation.
|
||||
4. Branch Promotion and Release Contract.
|
||||
|
||||
These follow-ups are not hidden scope for Spec 439.
|
||||
|
||||
## Open Questions
|
||||
|
||||
No blocking product, governance-process, or external-system question remains. Current repo governance permits the narrowly scoped amendment in the same feature PR. Remote Gitea target-ref verification remains a bounded conditional wiring/residual decision and does not block local correctness.
|
||||
|
||||
## Final Spec Gate - Requirements-Preparation Snapshot
|
||||
|
||||
- **Candidate Gate**: PASS.
|
||||
- **Spec Readiness Gate**: READY.
|
||||
- **Implementation Authorization**: AUTHORIZED.
|
||||
- **Constitution-Amendment Posture**: In scope as Phase 1, mandatory before every dependent mutation, and limited to the branch-topology rule plus required amendment metadata.
|
||||
- **Application implementation performed during requirements correction**: no.
|
||||
- **Next action recorded at requirements-preparation close-out**: Start a separate Spec 439 implementation loop at Phase 0, then apply and validate Phase 1 before any dependent work.
|
||||
|
||||
## Final Requirements-Preparation Statement
|
||||
|
||||
Spec 439 is implementation-ready. Its first mandatory implementation step is the narrow Constitution branch-topology amendment. After that gate succeeds, the same feature implements one fail-closed two-stage resolver, corrects local branch and evidence truth, and produces PASS only when execution, required evidence, and every blocking condition agree.
|
||||
265
specs/439-branch-topology-local-evidence-truth/tasks.md
Normal file
265
specs/439-branch-topology-local-evidence-truth/tasks.md
Normal file
@ -0,0 +1,265 @@
|
||||
# Tasks: Spec 439 - Branch Topology and Local Evidence Truth
|
||||
|
||||
**Input**: specs/439-branch-topology-local-evidence-truth/spec.md and plan.md
|
||||
**Tests**: Required during implementation; deterministic Pest 4 fixtures precede the corresponding implementation.
|
||||
**Scope**: Narrow Constitution branch amendment, directly conflicting authoritative instructions, repository branch/diff tooling, local test-lane evidence/result semantics, focused tests, and final implementation report only.
|
||||
**Implementation state**: Implementation and final reconciliation were executed. Paired clean-baseline attribution found no Spec 439 functional regression, no confirmed performance regression, no Fast Feedback hard-budget breach, and no local activation blocker matching the specified fail criteria. Final verdict is merge-ready locally subject to live verification that the Gitea check is not already a required merge boundary.
|
||||
|
||||
## Format
|
||||
|
||||
Task lines use:
|
||||
|
||||
- [ ] T### [P?] [US?] Action in exact file/component
|
||||
Phase: N | Refs: FR-... / INV-... | Depends: ... | Evidence: ...
|
||||
|
||||
- **[P]**: May run in parallel only when the task is file-disjoint and every earlier phase gate has passed.
|
||||
- **[US1-US5]**: Maps to the user stories in spec.md.
|
||||
- A task is marked complete only after its stated implementation evidence exists.
|
||||
|
||||
## Phase 0 - Revalidate Spec Readiness
|
||||
|
||||
**Purpose**: Reconfirm current repository and governance truth before any mutation.
|
||||
|
||||
- [x] T001 Revalidate the active package at specs/439-branch-topology-local-evidence-truth/, current branch, HEAD, dirty state, local dev/platform-dev/website-dev refs, Amendment Procedure, and absence of a mandatory separate Amendment Spec.
|
||||
Phase: 0 | Refs: FR-001, FR-002, FR-007, INV-001, INV-020 | Depends: none | Evidence: captured preflight facts for later Phase 8 report; no fetch and no file mutation.
|
||||
- [x] T002 Identify the exact obsolete universal-dev rule and every directly conflicting authoritative instruction in .specify/memory/constitution.md, Agents.md, and preflight-confirmed branch instruction files; baseline Specs 437/438.
|
||||
Phase: 0 | Refs: FR-003, FR-004, FR-006, FR-052, INV-003, INV-019 | Depends: T001 | Evidence: exact file/line inventory and checksums/diff baseline retained for Phase 8.
|
||||
|
||||
**Phase 0 Exit Gate**: The governance process, stale rule, direct consumers, defects, and protected historical specs are known. Any material mismatch is reconciled in the active artifacts before T003.
|
||||
|
||||
## Phase 1 - Apply the Narrow Constitution Branch-Topology Amendment
|
||||
|
||||
**Purpose**: Make the canonical governance rule match the declared three-branch topology before dependent implementation.
|
||||
|
||||
- [x] T003 [US1] Apply the narrowly scoped amendment to .specify/memory/constitution.md, changing only the universal feature-branch rule plus the required Sync Impact/version/date metadata.
|
||||
Phase: 1 | Refs: FR-001, FR-002, FR-004, INV-001, INV-003 | Depends: Phase 0 Exit Gate | Evidence: focused Constitution diff containing no unrelated principle change.
|
||||
- [x] T004 [US1] Define dev, platform-dev, and website-dev roles in .specify/memory/constitution.md.
|
||||
Phase: 1 | Refs: FR-003, FR-008, FR-009, FR-010, INV-004, INV-005, INV-006, INV-007 | Depends: T003 | Evidence: canonical three-role table or equivalent normative text.
|
||||
- [x] T005 [US1] Define platform, website, cross-stream/promotion, and repository-governance routing in .specify/memory/constitution.md, including explicit target requirements and no wrong-family/HEAD~1 fallback.
|
||||
Phase: 1 | Refs: FR-003, FR-011, FR-012, FR-013, FR-024, FR-025, INV-007, INV-008, INV-009 | Depends: T004 | Evidence: normative base/target/baseline and fail-closed language matching spec.md.
|
||||
- [x] T006 [US1] Validate the Constitution amendment against its Amendment Procedure, including rationale, impacted templates/specs/instructions, SemVer rationale, unchanged Ratified date, updated Last Amended, Sync Impact coherence, and unrelated-principle diff safety.
|
||||
Phase: 1 | Refs: FR-002, FR-004, FR-005, INV-002, INV-003 | Depends: T005 | Evidence: recorded Phase 1 PASS gate; failure stops every T007-T061 task.
|
||||
|
||||
**Hard Gate**: T006 MUST pass before any Phase 2-8 file is changed. No dependent task may run in parallel with Phase 1.
|
||||
|
||||
## Phase 2 - Synchronize Directly Conflicting Authoritative Instructions
|
||||
|
||||
**Purpose**: Align authoritative branch guidance only after the Constitution is canonical.
|
||||
|
||||
- [x] T007 [US1] Synchronize the confirmed conflicting branch rules in Agents.md and only preflight-confirmed authoritative branch instructions; retain valid promotion/platform guidance and residualize unrelated drift.
|
||||
Phase: 2 | Refs: FR-006, FR-033, INV-002, INV-003, INV-019 | Depends: T006 PASS | Evidence: focused instruction diff and search showing one canonical topology with no completed/deferred spec edits.
|
||||
|
||||
**Phase 2 Exit Gate**: Current authoritative branch instructions agree with the amended Constitution; unrelated docs/skills remain untouched or explicitly residualized.
|
||||
|
||||
## Phase 3 - Implement Branch-Family and Concrete-Reference Resolution
|
||||
|
||||
**Purpose**: Prove and implement the single two-stage resolver contract.
|
||||
|
||||
- [x] T008 [US2] Create or update apps/platform/tests/Feature/Guards/BranchTopologyContractTest.php with deterministic temporary-repository helpers and no network.
|
||||
Phase: 3 | Refs: FR-014-FR-028, FR-051, INV-010-INV-012, INV-020 | Depends: Phase 2 Exit Gate | Evidence: focused test file exists and initially fails against missing/old resolver behavior.
|
||||
- [x] T009 [US2] Add BS-001 through BS-004 fixtures for platform/website remote-ref priority and local-ref fallback in BranchTopologyContractTest.php.
|
||||
Phase: 3 | Refs: FR-009, FR-010, FR-021, FR-022 | Depends: T008 | Evidence: expected family/ref/merge-base assertions for both streams.
|
||||
- [x] T010 [US3] Add Stage 1 signal-priority and validation fixtures for explicit input, PR target, active spec metadata, and branch-name plus repository-scope hints.
|
||||
Phase: 3 | Refs: FR-014-FR-018 | Depends: T008 | Evidence: candidate-family assertions for each source without branch-name-only success.
|
||||
- [x] T011 [US3] Add BS-010 and BS-011 conflict/ambiguity fixtures, including explicit family platform plus PR target website-dev.
|
||||
Phase: 3 | Refs: FR-016, FR-017, FR-019, FR-020, INV-011 | Depends: T010 | Evidence: actionable non-zero conflict diagnostics and confirmation no diff ran.
|
||||
- [x] T012 [US2] Add BS-006 through BS-009 missing-ref and diverged-history fixtures for platform and website.
|
||||
Phase: 3 | Refs: FR-022, FR-024, FR-028, INV-008, INV-012 | Depends: T009 | Evidence: no dev/origin-dev/HEAD~1 fallback and feature-only changed paths.
|
||||
- [x] T013 [US3] Add BS-005 and BS-012 fixtures for explicit promotion to dev and rejection of an explicit ref from the wrong family.
|
||||
Phase: 3 | Refs: FR-011, FR-023, FR-025, INV-007, INV-009 | Depends: T010 | Evidence: explicit promotion succeeds; wrong-family ref fails before merge-base work.
|
||||
- [x] T014 [US2] Add success/failure diagnostics assertions in BranchTopologyContractTest.php for family, expected branch, selected ref, target, merge base, base range, conflicts, attempted refs, override guidance, and no-diff confirmation.
|
||||
Phase: 3 | Refs: FR-026, FR-027 | Depends: T011-T013 | Evidence: complete diagnostic contract assertions.
|
||||
- [x] T015 [US2] Implement Stage 1 family resolution in scripts/resolve-feature-base using exactly the priority and conflict rules in spec.md and plan.md.
|
||||
Phase: 3 | Refs: FR-014, FR-015, FR-016, FR-017, FR-018, FR-019, FR-020, INV-010, INV-011 | Depends: T010, T011 | Evidence: Stage 1 fixtures pass; no silent dev default.
|
||||
- [x] T016 [US2] Implement Stage 2 concrete-ref, ancestry, and merge-base resolution in scripts/resolve-feature-base.
|
||||
Phase: 3 | Refs: FR-021-FR-025, FR-028, INV-008, INV-010, INV-012 | Depends: T009, T012, T013, T015 | Evidence: remote/local priority, wrong-family rejection, and merge-base fixtures pass.
|
||||
- [x] T017 [US2] Implement stable human-readable and minimal machine-readable success/failure output in scripts/resolve-feature-base.
|
||||
Phase: 3 | Refs: FR-026, FR-027 | Depends: T014-T016 | Evidence: all diagnostic assertions pass.
|
||||
- [x] T018 [US2] Add shell syntax/portability and executable-contract coverage for scripts/resolve-feature-base in BranchTopologyContractTest.php or the smallest existing guard contract.
|
||||
Phase: 3 | Refs: NFR deterministic/portable tooling, FR-051 | Depends: T017 | Evidence: bash syntax and focused contract checks pass without network.
|
||||
- [x] T019 [US2] Run the focused resolver contract and capture BS-001 through BS-012 expected/actual outputs for Phase 8.
|
||||
Phase: 3 | Refs: SC-003, SC-005, SC-006, FR-054 | Depends: T018 | Evidence: twelve BS fixtures green with family/ref/merge-base/exit details.
|
||||
|
||||
**Phase 3 Exit Gate**: The two-stage resolver is green, fixed in scope, and uses no dev/HEAD~1 fallback for ordinary stream work.
|
||||
|
||||
## Phase 4 - Update Local Diff Guards and Repository-Side Workflow Declarations
|
||||
|
||||
**Purpose**: Make existing consumers use the resolver without expanding CI.
|
||||
|
||||
- [x] T020 [US2] Extend the focused guard contract for scripts/check-ui-productization-coverage before implementation, covering explicit base preservation, resolver output, fail-before-diff, and all working-tree change sources.
|
||||
Phase: 4 | Refs: FR-029-FR-031 | Depends: Phase 3 Exit Gate | Evidence: failing guard integration assertions.
|
||||
- [x] T021 [US2] Update scripts/check-ui-productization-coverage to consume the resolved family/ref/merge base and remove origin/dev/HEAD~1 stream fallbacks.
|
||||
Phase: 4 | Refs: FR-024, FR-029, FR-031, INV-008, INV-012 | Depends: T020 | Evidence: focused guard contract passes and prints the selected baseline.
|
||||
- [x] T022 [US2] Preserve committed, staged, unstaged, and untracked active-feature paths in scripts/check-ui-productization-coverage.
|
||||
Phase: 4 | Refs: FR-030 | Depends: T021 | Evidence: four change-source fixtures pass.
|
||||
- [x] T023 [P] [US3] Inspect .gitea/workflows/test-pr-fast-feedback.yml and existing workflow contracts to determine repo-locally whether the actual PR target can be supplied without live Gitea.
|
||||
Phase: 4 | Refs: FR-031, FR-032, FR-051, FR-053 | Depends: Phase 3 Exit Gate | Evidence: recorded local syntax/contract decision; no remote call.
|
||||
- [x] T024 [US3] If T023 is provable, minimally update .gitea/workflows/test-pr-fast-feedback.yml to pass the actual target through the unified contract; otherwise leave it unchanged and record the residual.
|
||||
Phase: 4 | Refs: FR-031, FR-032, FR-053 | Depends: T023 | Evidence: focused diff or explicit no-change residual; no trigger/lane/schedule/deployment change.
|
||||
- [x] T025 [US3] Update only directly affected workflow contract tests under apps/platform/tests/Feature/Guards/ to prove target coherence and no CI expansion.
|
||||
Phase: 4 | Refs: FR-032, FR-049, FR-053 | Depends: T024 | Evidence: workflow contracts pass and assert unchanged triggers/jobs/lanes/schedules.
|
||||
- [x] T026 [US2] Run the focused guard/workflow contract set and retain outputs for Phase 8.
|
||||
Phase: 4 | Refs: SC-009, FR-054 | Depends: T022, T025 | Evidence: guard and applicable workflow tests green; remote behavior explicitly unclaimed.
|
||||
|
||||
**Phase 4 Exit Gate**: Local diff truth uses the resolver; any workflow declaration change is minimal, repo-proven, and non-expansive.
|
||||
|
||||
## Phase 5 - Repair Local Test/Report Evidence Truth
|
||||
|
||||
**Purpose**: Preserve execution truth and make required evidence contradictions fail closed.
|
||||
|
||||
- [x] T027 [US4] Add an original non-zero execution persistence fixture to apps/platform/tests/Unit/Support/TestLaneReportTest.php.
|
||||
Phase: 5 | Refs: FR-034, FR-036, EF-001 | Depends: Phase 4 Exit Gate | Evidence: failing report metadata/CI summary assertions.
|
||||
- [x] T028 [US4] Add report-refresh failure and successful positive-control fixtures to TestLaneReportTest.php.
|
||||
Phase: 5 | Refs: FR-035, FR-037, FR-042, EF-001, EF-002 | Depends: T027 | Evidence: refresh cannot replace failure; valid pass remains pass.
|
||||
- [x] T029 [US5] Add JUnit existence, parse-validity, test/failure/error/skipped count, and evidence-status fixtures to TestLaneReportTest.php.
|
||||
Phase: 5 | Refs: FR-039, FR-041, EF-005, EF-006 | Depends: T027 | Evidence: missing/malformed/failure/error controls.
|
||||
- [x] T030 [US4] Add process/JUnit/report contradiction and dual test/report failure fixtures to TestLaneReportTest.php.
|
||||
Phase: 5 | Refs: FR-038, FR-040, EF-005, EF-007 | Depends: T028, T029 | Evidence: both contexts remain visible and non-zero.
|
||||
- [x] T031 [P] [US4] Extend apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php so lane/report wrappers must propagate derived final results.
|
||||
Phase: 5 | Refs: FR-034-FR-038, FR-049 | Depends: Phase 4 Exit Gate | Evidence: failing wrapper propagation assertions.
|
||||
- [x] T032 [US4] Minimally extend apps/platform/tests/Support/TestLaneReport.php with stored execution/evidence metadata while preserving existing valid consumers.
|
||||
Phase: 5 | Refs: FR-034, FR-042 | Depends: T027-T030 | Evidence: schema/positive-control tests pass.
|
||||
- [x] T033 [US4] Update TestLaneReport::finalizeLane() and TestLaneManifest::runLane() in apps/platform/tests/Support/ to preserve execution truth for later final-result derivation.
|
||||
Phase: 5 | Refs: FR-034, FR-036, FR-037 | Depends: T032 | Evidence: failed execution remains failed in stored artifacts.
|
||||
- [x] T034 [US4] Update TestLaneManifest::renderLatestReport() to consume stored execution truth and remove hard-coded success/unconditional zero behavior.
|
||||
Phase: 5 | Refs: FR-035, FR-037, EF-001, EF-002 | Depends: T033 | Evidence: refresh negative/positive controls pass.
|
||||
- [x] T035 [US5] Extend TestLaneReport::parseJUnit() with validity/count/status/path metadata.
|
||||
Phase: 5 | Refs: FR-039, FR-041, EF-005, EF-006 | Depends: T029, T032 | Evidence: all JUnit validity and diagnostic-path fixtures pass.
|
||||
- [x] T036 [US4] Preserve dual test/report failure and contradiction contexts in TestLaneReport.php and TestLaneManifest.php.
|
||||
Phase: 5 | Refs: FR-038, FR-040, EF-007 | Depends: T030, T033-T035 | Evidence: dual-failure and contradiction fixtures pass.
|
||||
- [x] T037 [US4] Run the focused evidence/report/command contracts and retain outputs for Phase 8.
|
||||
Phase: 5 | Refs: SC-004, SC-007, FR-054 | Depends: T031-T036 | Evidence: EF-001, EF-002, EF-005, EF-006, EF-007 controls green.
|
||||
|
||||
**Phase 5 Exit Gate**: Failed execution or invalid/contradictory required evidence cannot become PASS through report generation or refresh.
|
||||
|
||||
## Phase 6 - Repair Blocking/Advisory Budget Truth
|
||||
|
||||
**Purpose**: Make existing authoritative enforcement profiles determine the final result.
|
||||
|
||||
- [x] T038 [US5] Add a blocking fast-feedback pull-request final-exit fixture to apps/platform/tests/Feature/Guards/CiLaneFailureClassificationContractTest.php.
|
||||
Phase: 6 | Refs: FR-043, FR-045, EF-003 | Depends: Phase 5 Exit Gate | Evidence: failing non-zero final-exit assertion.
|
||||
- [x] T039 [US5] Add an advisory confidence overrun positive control to CiLaneFailureClassificationContractTest.php.
|
||||
Phase: 6 | Refs: FR-043, FR-046, EF-004 | Depends: T038 | Evidence: visible warning and zero absent another failure.
|
||||
- [x] T040 [P] [US5] Add missing/malformed active blocking-input coverage to apps/platform/tests/Unit/Support/TestLaneBudgetTest.php.
|
||||
Phase: 6 | Refs: FR-048 | Depends: Phase 5 Exit Gate | Evidence: failing fail-closed budget input assertions.
|
||||
- [x] T041 [US5] Implement one final-result derivation path in TestLaneReport.php and TestLaneManifest.php for execution, evidence, report generation, and authoritative blocking/advisory outcomes.
|
||||
Phase: 6 | Refs: FR-043, FR-045, FR-046, FR-048, FR-049, INV-014-INV-017 | Depends: T038-T040 | Evidence: EF-003/EF-004 and existing failure controls pass.
|
||||
- [x] T042 [US5] Update scripts/platform-test-lane, scripts/platform-test-report, and apps/platform/composer.json only where necessary to propagate the derived final result without duplicating policy.
|
||||
Phase: 6 | Refs: FR-044, FR-049 | Depends: T041, T031 | Evidence: wrapper/Composer contracts pass; no policy literals added.
|
||||
- [x] T043 [US5] Verify apps/platform/tests/Support/TestLaneBudget.php thresholds/profiles are unchanged and remain the sole authority.
|
||||
Phase: 6 | Refs: FR-043, FR-044, FR-047 | Depends: T041, T042 | Evidence: focused diff plus existing budget tests.
|
||||
- [x] T044 [US5] Run the focused budget/final-result contract set and retain outputs for Phase 8.
|
||||
Phase: 6 | Refs: SC-004, SC-007, SC-008, FR-054 | Depends: T042, T043 | Evidence: EF-003 and EF-004 controls green with expected final exits.
|
||||
|
||||
**Phase 6 Exit Gate**: Blocking profiles block, advisory profiles warn, thresholds remain unchanged, and every reusable entry point shares one final-result contract.
|
||||
|
||||
## Phase 7 - Complete Negative Fixtures and Run Focused Proof
|
||||
|
||||
**Purpose**: Execute all required local proof and scope guards after implementation convergence.
|
||||
|
||||
- [x] T045 Complete and reconcile every BS-001 through BS-012 fixture in BranchTopologyContractTest.php, removing duplicate or weaker controls.
|
||||
Phase: 7 | Refs: FR-014-FR-028, INV-010-INV-012, SC-003, SC-005, SC-006 | Depends: Phase 6 Exit Gate | Evidence: twelve unique branch fixtures with expected diagnostics/exits.
|
||||
- [x] T046 Complete and reconcile every EF-001 through EF-007 fixture across TestLaneReportTest.php, TestLaneBudgetTest.php, and CiLaneFailureClassificationContractTest.php.
|
||||
Phase: 7 | Refs: FR-034-FR-049, INV-013-INV-017, SC-004, SC-007, SC-008 | Depends: Phase 6 Exit Gate | Evidence: seven unique evidence fixtures with expected statuses/exits.
|
||||
- [x] T047 Add/update apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php proving canonical roles, no universal-dev wording, amendment metadata coherence, and no unrelated-principle drift.
|
||||
Phase: 7 | Refs: FR-001-FR-007, INV-001-INV-003, SC-001, SC-002 | Depends: Phase 6 Exit Gate | Evidence: governance contract proves final amendment content, metadata, authorized boundary, unrelated-scope safety, and dependent-instruction consistency; it is final-state corroboration, not historical chronology.
|
||||
- [x] T048 Run the focused Constitution, instruction, branch resolver, and changed-file guard contract tests via Sail.
|
||||
Phase: 7 | Refs: FR-001-FR-033, INV-001-INV-012, SC-001-SC-006 | Depends: T045, T047 | Evidence: recorded commands, counts, assertions, and exits.
|
||||
- [x] T049 Run TestLaneReportTest.php, TestLaneBudgetTest.php, CiLaneFailureClassificationContractTest.php, and TestLaneCommandContractTest.php via Sail.
|
||||
Phase: 7 | Refs: FR-034-FR-049, INV-013-INV-017, SC-004, SC-007, SC-008 | Depends: T046 | Evidence: recorded commands, counts, assertions, and exits.
|
||||
- [x] T050 Run TestLaneArtifactsContractTest.php, TestLaneManifestTest.php, and only directly affected workflow contract tests via Sail.
|
||||
Phase: 7 | Refs: FR-041, FR-049, SC-009 | Depends: T026, T046 | Evidence: artifact/manifest/workflow contracts green.
|
||||
- [x] T051 Run ./scripts/platform-test-lane fast-feedback with pull-request context and confidence with mainline-push context; do not add Heavy Governance to regain proof.
|
||||
Phase: 7 | Refs: FR-043-FR-049, INV-014-INV-017, TEST-GOV-001, SC-007-SC-009 | Depends: T048-T050 | Evidence: actual lane/budget/baseline/trend exits and artifacts.
|
||||
- [x] T052 Run Sail Pint on dirty PHP files and document the result.
|
||||
Phase: 7 | Refs: FR-050, FR-054, INV-018, INV-019, Constitution Quality Gates | Depends: T051 | Evidence: formatter result with no unrelated file changes.
|
||||
- [x] T053 Run git diff --check, allowed-path checks, amendment final-state and evidence-sufficiency review, and byte/diff verification that Specs 437/438 and forbidden runtime/UI paths are unchanged.
|
||||
Phase: 7 | Refs: FR-004, FR-050, FR-052, SC-002, SC-010, SC-011, INV-003, INV-018, INV-019 | Depends: T052 | Evidence: clean diff/scope/final-contract/history-safety checks; no wall-clock chronology is claimed.
|
||||
- [x] T054 Verify no fetch, remote mutation, PR creation, branch protection, deployment activation, long-lived branch merge/rebase/synchronization, or remote-verification claim occurred.
|
||||
Phase: 7 | Refs: FR-051, FR-053, INV-020 | Depends: T053 | Evidence: explicit local-only close-out statement.
|
||||
|
||||
**Phase 7 Exit Gate**: Every local invariant and fixture is green or honestly failed; scope and phase-order checks are clean.
|
||||
|
||||
## Phase 8 - Produce Implementation Report and Final Reconciliation
|
||||
|
||||
**Purpose**: Create the implementation report only after all producing evidence exists.
|
||||
|
||||
- [x] T055 Create specs/439-branch-topology-local-evidence-truth/implementation-report.md with branch/HEAD/dirty-state/preflight and per-file FR/INV traceability.
|
||||
Phase: 8 | Refs: FR-054 | Depends: Phase 7 Exit Gate | Evidence: report exists with complete preflight and changed-file map.
|
||||
- [x] T056 Record the Constitution amendment rationale, impacted artifacts, SemVer decision, Last Amended value, focused diff, Phase 1 validation, and amendment-first evidence sufficiency, including process attestation, final-state corroboration, and checkpoint status.
|
||||
Phase: 8 | Refs: FR-001-FR-007, SC-001, SC-002, INV-001-INV-003 | Depends: T055 | Evidence: the report records the final amendment, exact boundary and unrelated-scope controls, synchronized dependents, absence of conflicting final state, and the process attestation. No pre-mutation commit/tag/patch/snapshot checkpoint was required; no independent historical chronology is retroactively claimed.
|
||||
- [x] T057 Record BS-001 through BS-012 with topology, signals, selected family/ref, target, merge base, changed paths, diagnostics, expected/actual result, and exit.
|
||||
Phase: 8 | Refs: SC-003, SC-005, SC-006, FR-054 | Depends: T019, T045, T055 | Evidence: complete twelve-row BS matrix.
|
||||
- [x] T058 Record EF-001 through EF-007 with execution, evidence, report, budget, expected/actual final status, and exit.
|
||||
Phase: 8 | Refs: SC-004, SC-007, SC-008, FR-054 | Depends: T037, T044, T046, T055 | Evidence: complete seven-row EF matrix.
|
||||
- [x] T059 Record test/lane rationale, actual results, test-governance outcome, budget/baseline/trend impact, remote Gitea/website-delivery/deployment residuals, and unrelated drift disposition.
|
||||
Phase: 8 | Refs: TEST-GOV-001, FR-053, FR-054 | Depends: T051, T054-T058 | Evidence: `evidence/baseline-attribution.json` and the report record native and identical-selection paired runs, exact JUnit multiset deltas, unchanged budgets, baseline debt, activation impact, and bounded external residuals.
|
||||
- [x] T060 Record Product Surface/browser/Human Product Sanity N/A, Livewire v4 compliance, provider registration unchanged at bootstrap/providers.php, global-search/destructive-action/asset/deployment posture, visible complexity, and completed-spec safety.
|
||||
Phase: 8 | Refs: PSC-001, UI-COV-001, FR-050, FR-052 | Depends: T055 | Evidence: complete no-surface/no-runtime implementation-report close-out.
|
||||
- [x] T061 Reconcile every FR-001 through FR-054, INV-001 through INV-020, T001 through T061, BS/EF fixture, focused test, paired baseline attribution, lane, budget/activation result, residual, and Definition-of-Done item; set the exact final verdict without converting a failed lane into PASS.
|
||||
Phase: 8 | Refs: FR-054, SC-012 | Depends: T056-T060 | Evidence: `evidence/baseline-attribution.json`, current test evidence, task state, residuals, and `implementation-report.md` agree on the final verdict.
|
||||
|
||||
## Dependencies and Execution Order
|
||||
|
||||
- Phase 0 blocks Phase 1.
|
||||
- T006 is the global hard gate and blocks every T007-T061 task.
|
||||
- Phase 2 blocks Phase 3 and all dependent implementation.
|
||||
- Phase 3 blocks the guard portion of Phase 4.
|
||||
- Phase 4 must finish before final branch/workflow proof.
|
||||
- Phase 5 blocks Phase 6 because final-result derivation depends on stored execution/evidence truth.
|
||||
- Phase 6 blocks consolidated EF proof.
|
||||
- Phase 7 follows all implementation phases.
|
||||
- Phase 8 is strictly last and is the only phase that creates implementation-report.md.
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
- No task is parallelizable before T006.
|
||||
- T023 may inspect workflow syntax while T020-T022 work on the guard after Phase 3 passes.
|
||||
- T031 may add wrapper contract coverage while T027-T030 work in the Unit report test file.
|
||||
- T040 may add budget-input tests after Phase 5 while T038-T039 work in a different Feature test file.
|
||||
- Focused proof tasks T048-T050 run sequentially unless a future task update marks a file-disjoint command explicitly [P].
|
||||
- All shared edits to .specify/memory/constitution.md, Agents.md, scripts/resolve-feature-base, TestLaneReport.php, and TestLaneManifest.php are single-owner.
|
||||
- [P] never overrides a phase gate or same-file sequencing.
|
||||
|
||||
## Requirement and Evidence Coverage
|
||||
|
||||
| Contract | Tasks |
|
||||
|---|---|
|
||||
| FR-001-FR-007 / INV-001-INV-003 | T001-T007, T047, T056, T061 |
|
||||
| FR-008-FR-013 / INV-004-INV-009 | T004-T005, T009-T013, T045, T057 |
|
||||
| FR-014-FR-028 / INV-010-INV-012 | T008-T019, T045, T048, T057 |
|
||||
| FR-029-FR-033 | T007, T020-T026, T048, T059 |
|
||||
| FR-034-FR-042 / INV-013, INV-016-INV-017 | T027-T037, T046, T049-T050, T058 |
|
||||
| FR-043-FR-049 / INV-014-INV-015 | T038-T046, T049-T051, T058-T059 |
|
||||
| FR-050-FR-054 / INV-018-INV-020 | T001-T002, T053-T061 |
|
||||
| BS-001-BS-012 | T009-T014, T019, T045, T048, T057 |
|
||||
| EF-001-EF-007 | T027-T030, T038-T041, T044, T046, T049-T050, T058 |
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment remains Unit for evidence/result/budget logic and bounded Feature for governance/branch/wrapper/workflow contracts.
|
||||
- [x] No Heavy-Governance, Browser, or PostgreSQL family is added.
|
||||
- [x] Temporary Git/JUnit/report fixtures remain deterministic, network-free, and database-free.
|
||||
- [x] Shared fixtures/helpers/default context are not widened.
|
||||
- [x] Focused commands cover the change without unrelated lane cost.
|
||||
- [x] Browser proof remains N/A - no rendered UI surface changed.
|
||||
- [x] Human Product Sanity remains N/A and Product Surface implementation-report fields are completed in Phase 8.
|
||||
- [x] Existing thresholds remain unchanged and blocking/advisory outcomes are reported honestly.
|
||||
- [x] Final test-governance outcome is recorded in Phase 8.
|
||||
|
||||
## Explicit Non-Goals During Task Execution
|
||||
|
||||
- Do not start T007 or any later task before T006 passes.
|
||||
- Do not implement Spec 438 or modify completed/deferred historical specs.
|
||||
- Do not change product runtime, website runtime, UI, routes, models, migrations, services, policies, jobs, providers, customer output, RBAC, OperationRun, or product audit behavior.
|
||||
- Do not create a generic branch/release/governance framework.
|
||||
- Do not perform general Constitution, instruction, skill, architecture, security, Ledger, Roadmap, or Spec Candidates cleanup.
|
||||
- Do not activate new Gitea CI, branch protection, deployment, heavy/browser/PostgreSQL lanes, or merge gates.
|
||||
- Do not fetch, mutate remotes, merge, rebase, synchronize, rename, or delete long-lived branches.
|
||||
- Do not relax budget thresholds or reclassify blocking policy merely to obtain green output.
|
||||
|
||||
## Implementation Tracking Note
|
||||
|
||||
- The earlier Spec-Correction run left T001-T061 open; the current implementation loop updates checkboxes only after evidence exists.
|
||||
- At requirements-preparation close-out no implementation report existed; it was created only during Phase 8.
|
||||
- Runtime, browser, and lane evidence is claimed only by the producing tasks below.
|
||||
Loading…
Reference in New Issue
Block a user