phase 16 implementation
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const manifestPath = path.join(root, 'PHASE_14_PACKAGE_MANIFEST_v1.0.json');
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
]);
|
||||
const excludedFiles = new Set([
|
||||
'PHASE_14_PACKAGE_MANIFEST_v1.0.json',
|
||||
'package-lock.json',
|
||||
'tsconfig.tsbuildinfo',
|
||||
]);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(root, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push({ fullPath, relative });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = (await walk(root)).sort((a, b) => a.relative.localeCompare(b.relative));
|
||||
const entries = [];
|
||||
for (const file of files) {
|
||||
const contents = await readFile(file.fullPath);
|
||||
const details = await stat(file.fullPath);
|
||||
entries.push({
|
||||
path: file.relative,
|
||||
bytes: details.size,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
});
|
||||
}
|
||||
const manifest = {
|
||||
schema_version: '1.0',
|
||||
project: 'RentalDriveGo',
|
||||
phase: 14,
|
||||
generated_at: '2026-06-25',
|
||||
excludes: [...excludedDirectories, ...excludedFiles],
|
||||
file_count: entries.length,
|
||||
files: entries,
|
||||
};
|
||||
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`Generated Phase 14 manifest for ${entries.length} files.`);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const manifestPath = path.join(root, 'PHASE_15_PACKAGE_MANIFEST_v1.0.json');
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
'__pycache__',
|
||||
]);
|
||||
const excludedFiles = new Set([
|
||||
'PHASE_15_PACKAGE_MANIFEST_v1.0.json',
|
||||
'package-lock.json',
|
||||
'tsconfig.tsbuildinfo',
|
||||
'docs/quality/phase15-command-logs/12-phase15-package-manifest.log',
|
||||
]);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(root, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push({ fullPath, relative });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = (await walk(root)).sort((a, b) => a.relative.localeCompare(b.relative));
|
||||
const entries = [];
|
||||
for (const file of files) {
|
||||
const contents = await readFile(file.fullPath);
|
||||
const details = await stat(file.fullPath);
|
||||
entries.push({
|
||||
path: file.relative,
|
||||
bytes: details.size,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
});
|
||||
}
|
||||
const manifest = {
|
||||
schema_version: '1.0',
|
||||
project: 'RentalDriveGo',
|
||||
phase: 15,
|
||||
generated_at: '2026-06-25',
|
||||
excludes: [...excludedDirectories, ...excludedFiles],
|
||||
file_count: entries.length,
|
||||
files: entries,
|
||||
};
|
||||
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`Generated Phase 15 manifest for ${entries.length} files.`);
|
||||
Executable
+621
@@ -0,0 +1,621 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
import platform
|
||||
import subprocess
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'docs' / 'phase15'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
DATE = '2026-06-25'
|
||||
|
||||
|
||||
def write(name: str, text: str):
|
||||
(OUT / name).write_text(text.rstrip() + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def report(title: str, status: str, summary: str, executed: list[str], blocked: list[str], disposition: str, extra: str = '') -> str:
|
||||
executed_text = '\n'.join(f'- {item}' for item in executed) if executed else '- None.'
|
||||
blocked_text = '\n'.join(f'- {item}' for item in blocked) if blocked else '- None.'
|
||||
return f'''# {title}
|
||||
|
||||
**Date:** {DATE}
|
||||
**Status:** {status}
|
||||
|
||||
## Summary
|
||||
|
||||
{summary}
|
||||
|
||||
## Executed evidence
|
||||
|
||||
{executed_text}
|
||||
|
||||
## Unexecuted or blocked evidence
|
||||
|
||||
{blocked_text}
|
||||
|
||||
## Release disposition
|
||||
|
||||
{disposition}
|
||||
{extra}
|
||||
'''
|
||||
|
||||
|
||||
def csv_write(name: str, fields: list[str], rows: list[dict[str, str]]):
|
||||
with (OUT / name).open('w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def read_json(path: str):
|
||||
return json.loads((ROOT / path).read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def get_version(command: list[str]) -> str:
|
||||
try:
|
||||
return subprocess.check_output(command, text=True, stderr=subprocess.STDOUT).strip()
|
||||
except Exception as exc:
|
||||
return f'unavailable: {exc}'
|
||||
|
||||
|
||||
static_result = read_json('docs/quality/PHASE_15_STATIC_AUDIT_RESULTS_v1.0.json')
|
||||
baseline_static = read_json('docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json')
|
||||
phase14_result = read_json('docs/quality/PHASE_14_VALIDATION_RESULTS_v1.0.json')
|
||||
with (ROOT / 'docs/IMPLEMENTATION_ISSUE_REGISTER_v1.5.csv').open(newline='', encoding='utf-8-sig') as f:
|
||||
issues = list(csv.DictReader(f))
|
||||
unresolved = [row for row in issues if 'resolved' not in row['Status'].lower()]
|
||||
unresolved_counts = Counter(row['Severity'] for row in unresolved)
|
||||
|
||||
# Command matrix
|
||||
command_fields = [
|
||||
'ID','Command','Environment','Status','Exit_code','Duration_seconds','Test_count','Pass_count',
|
||||
'Failure_count','Skipped_count','Warning_count','Evidence_path','Related_defects','Notes'
|
||||
]
|
||||
command_rows = [
|
||||
{'ID':'C15-001','Command':'node scripts/validate-phase14-manifest.mjs','Environment':'Fresh extraction of supplied Phase 14 ZIP; Node 22.16.0','Status':'Passed','Exit_code':'0','Duration_seconds':'0.22','Test_count':'466 files','Pass_count':'466','Failure_count':'0','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/phase15-command-logs/02-phase14-manifest-baseline.log','Related_defects':'','Notes':'Verified the supplied archive before Phase 15 modifications.'},
|
||||
{'ID':'C15-002','Command':'corepack prepare pnpm@11.9.0 --activate','Environment':'Debian 13; Node 22.16.0; Corepack 0.32.0; registry DNS unavailable','Status':'Blocked','Exit_code':'1','Duration_seconds':'0.17','Test_count':'1','Pass_count':'0','Failure_count':'1','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/PHASE_15_TOOLCHAIN_SETUP.log','Related_defects':'P14-009; P15-005','Notes':'Could not download the pinned package manager.'},
|
||||
{'ID':'C15-003','Command':'pnpm install --frozen-lockfile','Environment':'Required Node 24.17.0 and pnpm 11.9.0','Status':'Not executed - prerequisite blocked','Exit_code':'','Duration_seconds':'','Test_count':'','Pass_count':'','Failure_count':'','Skipped_count':'','Warning_count':'','Evidence_path':'docs/quality/PHASE_15_TOOLCHAIN_SETUP.log','Related_defects':'P14-009; P15-005','Notes':'No pnpm binary and no registry DNS. No dependency update was attempted.'},
|
||||
{'ID':'C15-004','Command':'node scripts/run-validations.mjs','Environment':'Current Phase 15 tree; Node 22.16.0; no installed packages required','Status':'Passed','Exit_code':'0','Duration_seconds':'1.52','Test_count':'16 validators','Pass_count':'16','Failure_count':'0','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/phase15-command-logs/10-final-validations.log','Related_defects':'P15-001; P15-002; P15-003; P15-004','Notes':'Repository-native dependency-free validators.'},
|
||||
{'ID':'C15-005','Command':'python3 scripts/phase15-static-audit.py --root /mnt/data/phase14_baseline','Environment':'Fresh Phase 14 extraction; Python 3.13.5','Status':'Failed as expected - baseline findings','Exit_code':'1','Duration_seconds':'1.37','Test_count':str(baseline_static['metrics']['file_count'])+' files','Pass_count':'','Failure_count':'11 findings','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json','Related_defects':'P15-001; P15-002; P15-003; P15-004','Notes':'Found 4 High and 7 Medium defects before fixes.'},
|
||||
{'ID':'C15-006','Command':'python3 scripts/phase15-static-audit.py','Environment':'Current Phase 15 tree; Python 3.13.5','Status':'Passed','Exit_code':'0','Duration_seconds':'1.38','Test_count':str(static_result['metrics']['file_count'])+' files','Pass_count':str(static_result['metrics']['file_count']),'Failure_count':'0','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/phase15-command-logs/13-final-static-audit.log','Related_defects':'P15-001; P15-002; P15-003; P15-004','Notes':'No findings under the same audit after remediation.'},
|
||||
{'ID':'C15-007','Command':'node --experimental-strip-types media-type behavior assertions','Environment':'Node 22.16.0','Status':'Passed with runtime warnings','Exit_code':'0','Duration_seconds':'1.37','Test_count':'8 assertions','Pass_count':'8','Failure_count':'0','Skipped_count':'0','Warning_count':'2','Evidence_path':'docs/quality/phase15-command-logs/08-content-type-behavior.log','Related_defects':'P15-002','Notes':'Warnings concern experimental type stripping and do not affect the assertions.'},
|
||||
{'ID':'C15-008','Command':'node --check scripts; node --experimental-strip-types --check TypeScript; python3 -m py_compile audit','Environment':'Node 22.16.0; Python 3.13.5','Status':'Passed','Exit_code':'0','Duration_seconds':'6.76','Test_count':'99 files','Pass_count':'99','Failure_count':'0','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/phase15-command-logs/09-syntax-check.log','Related_defects':'','Notes':'33 JavaScript modules, 64 TypeScript modules, and two Python scripts.'},
|
||||
]
|
||||
blocked_commands = [
|
||||
('C15-009','pnpm format:check','Formatting validation'),
|
||||
('C15-010','pnpm lint','ESLint'),
|
||||
('C15-011','pnpm typecheck','Next type generation and strict TypeScript'),
|
||||
('C15-012','pnpm test:unit','Unit tests and coverage'),
|
||||
('C15-013','pnpm test:integration','Integration tests'),
|
||||
('C15-014','pnpm test:browser','Functional browser tests'),
|
||||
('C15-015','pnpm test:a11y','Automated accessibility tests'),
|
||||
('C15-016','pnpm test:visual','Visual-regression tests'),
|
||||
('C15-017','pnpm test:e2e','Aggregate Playwright tests'),
|
||||
('C15-018','pnpm test:security','Package-script security validation; underlying static validator ran through C15-004'),
|
||||
('C15-019','pnpm test:privacy','Package-script privacy validation; underlying static validator ran through C15-004'),
|
||||
('C15-020','SITE_ORIGIN=https://approved.example PUBLIC_RELEASE_APPROVED=false pnpm build','Production build'),
|
||||
('C15-021','pnpm audit --audit-level low','Registry-backed dependency audit'),
|
||||
('C15-022','pnpm exec playwright install --with-deps chromium firefox webkit','Pinned browser installation'),
|
||||
('C15-023','Performance audit commands','Lighthouse/Web Vitals and route timing'),
|
||||
('C15-024','Production-like deployment and smoke commands','Deployment rehearsal'),
|
||||
('C15-025','Rollback platform commands','Rollback rehearsal'),
|
||||
]
|
||||
for cid, cmd, note in blocked_commands:
|
||||
command_rows.append({'ID':cid,'Command':cmd,'Environment':'Exact pinned toolchain and installed dependencies required','Status':'Not executed - clean install blocked','Exit_code':'','Duration_seconds':'','Test_count':'','Pass_count':'','Failure_count':'','Skipped_count':'','Warning_count':'','Evidence_path':'docs/quality/PHASE_15_TOOLCHAIN_SETUP.log','Related_defects':'P15-005','Notes':note})
|
||||
command_rows.append({'ID':'C15-026','Command':'node scripts/generate-phase15-manifest.mjs && node scripts/validate-phase15-manifest.mjs','Environment':'Final Phase 15 repository; Node 22.16.0; dependency-free','Status':'Passed','Exit_code':'0','Duration_seconds':'0.52','Test_count':'548 files','Pass_count':'548','Failure_count':'0','Skipped_count':'0','Warning_count':'0','Evidence_path':'docs/quality/phase15-command-logs/12-phase15-package-manifest.log','Related_defects':'','Notes':'The validation log is explicitly excluded from its own manifest to avoid a self-referential checksum.'})
|
||||
csv_write('COMMAND_MATRIX_v1.0.csv', command_fields, command_rows)
|
||||
|
||||
# Defect register
|
||||
fields = ['ID','Summary','Severity','User_impact','Business_impact','Security_impact','Privacy_impact','Accessibility_impact','Affected_locale','Affected_theme','Affected_browser','Affected_viewport','Reproduction_steps','Expected_behavior','Actual_behavior','Evidence','Owner','Status','Release_disposition','Resolution_commit','Retest_result']
|
||||
defects = [
|
||||
{'ID':'P15-001','Summary':'Two release-evidence CSV files had inconsistent column counts','Severity':'High','User_impact':'Indirect; evidence could be misread or fields shifted','Business_impact':'Release decisions could use corrupted issue or QA records','Security_impact':'None direct','Privacy_impact':'None direct','Accessibility_impact':'None direct','Affected_locale':'All','Affected_theme':'All','Affected_browser':'All','Affected_viewport':'All','Reproduction_steps':'Parse all repository CSV files with a standards-compliant CSV reader','Expected_behavior':'Every row matches the header width','Actual_behavior':'P14-009 had 11 fields instead of 9; P14-M10 had 10 instead of 11','Evidence':'PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json','Owner':'Engineering + QA','Status':'Resolved','Release_disposition':'Fixed in Phase 15','Resolution_commit':'Unavailable; supplied archive had no Git metadata','Retest_result':'All CSV files parse; C15-004 and C15-006 pass'},
|
||||
{'ID':'P15-002','Summary':'Demo API accepted JSON look-alike media types by prefix','Severity':'Medium','User_impact':'Malformed clients could reach JSON parsing instead of receiving 415','Business_impact':'Ambiguous API contract and weaker request validation','Security_impact':'Broader-than-documented content-type acceptance','Privacy_impact':'No direct data exposure identified','Accessibility_impact':'None','Affected_locale':'All','Affected_theme':'All','Affected_browser':'All clients','Affected_viewport':'N/A','Reproduction_steps':'Send Content-Type application/jsonp','Expected_behavior':'415 unsupported_content_type','Actual_behavior':'Prefix check treated it as JSON','Evidence':'PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json','Owner':'Engineering + Security','Status':'Resolved','Release_disposition':'Fixed in Phase 15','Resolution_commit':'Unavailable; supplied archive had no Git metadata','Retest_result':'Eight dependency-free assertions pass; Vitest regression authored but not run'},
|
||||
{'ID':'P15-003','Summary':'Visual tests used a stale theme key and inconsistent snapshot project scope','Severity':'Medium','User_impact':'Dark-mode regressions could be recorded under the wrong theme','Business_impact':'Unreliable visual approval evidence','Security_impact':'None','Privacy_impact':'None','Accessibility_impact':'Theme contrast regressions could be missed','Affected_locale':'EN/FR/AR','Affected_theme':'Dark/system','Affected_browser':'All Playwright projects','Affected_viewport':'Configured visual matrix','Reproduction_steps':'Inspect component-system visual setup and run snapshots outside Chromium','Expected_behavior':'Canonical theme key; canonical Chromium snapshots only','Actual_behavior':'Unused storage key and non-canonical project execution','Evidence':'PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json','Owner':'Engineering + Design QA','Status':'Resolved','Release_disposition':'Fixed; browser retest still blocked','Resolution_commit':'Unavailable; supplied archive had no Git metadata','Retest_result':'Static regression validator passes; browser snapshots not executed'},
|
||||
{'ID':'P15-004','Summary':'CI omitted the demo-service integration test suite','Severity':'High','User_impact':'Server-side conversion failures could merge undetected','Business_impact':'Primary lead workflow lacked CI integration protection','Security_impact':'Adapter error/timeout/idempotency regressions could escape CI','Privacy_impact':'Boundary regressions could escape CI','Accessibility_impact':'None direct','Affected_locale':'All','Affected_theme':'All','Affected_browser':'Server boundary','Affected_viewport':'N/A','Reproduction_steps':'Inspect Phase 14 CI quality job','Expected_behavior':'CI runs pnpm test:integration','Actual_behavior':'Only unit and Playwright suites were configured','Evidence':'PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json','Owner':'Engineering + QA','Status':'Resolved','Release_disposition':'CI command added; execution pending connected CI','Resolution_commit':'Unavailable; supplied archive had no Git metadata','Retest_result':'Phase 15 validator confirms command presence; integration suite not locally executed'},
|
||||
{'ID':'P15-005','Summary':'Final Phase 15 dependency-backed, browser, performance, deployment, rollback, and manual QA evidence is incomplete','Severity':'High','User_impact':'Accessibility, browser, performance, and conversion defects may remain undiscovered','Business_impact':'Release readiness cannot be established','Security_impact':'Dependency and dynamic security checks are stale or absent','Privacy_impact':'Runtime analytics/consent behavior lacks current browser evidence','Accessibility_impact':'Manual WCAG, keyboard, screen-reader, zoom, and reflow evidence absent','Affected_locale':'EN/FR/AR','Affected_theme':'Light/dark/system','Affected_browser':'Chromium/Firefox/WebKit/mobile engines','Affected_viewport':'1440/1024/768/390/320 and zoom states','Reproduction_steps':'Attempt pinned toolchain activation and frozen install','Expected_behavior':'Exact toolchain installs and all Phase 15 gates execute','Actual_behavior':'Host has Node 22.16.0, no pnpm, no registry DNS, and no project dependencies','Evidence':'PHASE_15_TOOLCHAIN_SETUP.log; COMMAND_MATRIX_v1.0.csv','Owner':'Engineering + QA + Accessibility + Security','Status':'Open - environment blocked','Release_disposition':'Blocks Phase 15 acceptance and Phase 16 readiness','Resolution_commit':'N/A','Retest_result':'Not retested; requires connected pinned CI and human QA'},
|
||||
]
|
||||
csv_write('DEFECT_REGISTER_v1.0.csv', fields, defects)
|
||||
|
||||
# Release gates
|
||||
fields = ['Gate','Severity','Status','Evidence','Owner','Exit_criteria','Accepted_exception']
|
||||
gates = [
|
||||
{'Gate':'Supplied Phase 14 package integrity','Severity':'High','Status':'Passed','Evidence':'C15-001','Owner':'Engineering','Exit_criteria':'Manifest validates fresh extraction','Accepted_exception':'No'},
|
||||
{'Gate':'Phase 15 static validators','Severity':'High','Status':'Passed','Evidence':'C15-004','Owner':'Engineering + QA','Exit_criteria':'All dependency-free validators pass','Accepted_exception':'No'},
|
||||
{'Gate':'Repository integrity and secret/PII fixture audit','Severity':'High','Status':'Passed','Evidence':'C15-006','Owner':'Engineering + Security + Privacy','Exit_criteria':'Zero Critical/High/Medium findings','Accepted_exception':'No'},
|
||||
{'Gate':'Exact Node 24.17.0 and pnpm 11.9.0 frozen install','Severity':'High','Status':'Blocked','Evidence':'C15-002; C15-003','Owner':'Engineering + Security','Exit_criteria':'Connected clean environment completes frozen install','Accepted_exception':'No'},
|
||||
{'Gate':'Format, lint, strict typecheck, unit, integration, and production build on final manifest','Severity':'High','Status':'Blocked','Evidence':'C15-009 through C15-020','Owner':'Engineering + QA','Exit_criteria':'All commands exit zero on final manifest','Accepted_exception':'No'},
|
||||
{'Gate':'Dependency and license audit','Severity':'High','Status':'Blocked','Evidence':'C15-021','Owner':'Security + Legal','Exit_criteria':'Registry-backed audit and configured license check pass','Accepted_exception':'No'},
|
||||
{'Gate':'Cross-browser functional execution','Severity':'High','Status':'Blocked','Evidence':'C15-014; P14-008','Owner':'QA','Exit_criteria':'Chromium, Firefox, WebKit, mobile Chrome, mobile Safari pass','Accepted_exception':'No'},
|
||||
{'Gate':'Automated accessibility execution','Severity':'High','Status':'Blocked','Evidence':'C15-015; P14-008','Owner':'Accessibility + QA','Exit_criteria':'Axe matrix passes with reviewed exceptions','Accepted_exception':'No'},
|
||||
{'Gate':'Manual keyboard, screen-reader, zoom, reflow, forced-colors, and RTL review','Severity':'High','Status':'Blocked','Evidence':'MANUAL_TEST_MATRIX_v1.0.csv','Owner':'Accessibility + QA + Localization','Exit_criteria':'Human matrix completed with evidence','Accepted_exception':'No'},
|
||||
{'Gate':'Visual-regression baseline generation and approval','Severity':'High','Status':'Blocked','Evidence':'C15-016; VISUAL_REGRESSION_REPORT_v1.0.md','Owner':'Design + QA','Exit_criteria':'Pinned Chromium baselines generated and all diffs reviewed','Accepted_exception':'No'},
|
||||
{'Gate':'Performance and bundle budgets','Severity':'High','Status':'Blocked','Evidence':'PERFORMANCE_REPORT_v1.0.md; BUNDLE_ANALYSIS_REPORT_v1.0.md','Owner':'Engineering','Exit_criteria':'Final build measured against approved budgets','Accepted_exception':'No'},
|
||||
{'Gate':'Production-like deployment and rollback rehearsal','Severity':'High','Status':'Blocked','Evidence':'DEPLOYMENT_REHEARSAL_REPORT_v1.0.md; ROLLBACK_REHEARSAL_REPORT_v1.0.md','Owner':'Infrastructure + Engineering','Exit_criteria':'Documented rehearsal succeeds and is timed','Accepted_exception':'No'},
|
||||
{'Gate':'Approved CRM/lead destination','Severity':'High','Status':'Blocked','Evidence':'P14-001','Owner':'Sales Ops + Engineering','Exit_criteria':'Approved vendor, credentials, mapping, retries, duplicates, sandbox, monitoring','Accepted_exception':'No'},
|
||||
{'Gate':'Approved legal/privacy/form processing','Severity':'High','Status':'Blocked','Evidence':'P14-002','Owner':'Legal + Privacy','Exit_criteria':'Localized notice, legal basis, consent, retention, deletion, recipients, transfers approved','Accepted_exception':'No'},
|
||||
{'Gate':'Approved analytics provider and consent','Severity':'High','Status':'Blocked','Evidence':'P14-003','Owner':'Analytics + Legal','Exit_criteria':'Provider, DPA, consent, region, retention, event policy approved','Accepted_exception':'No'},
|
||||
{'Gate':'Distributed idempotency and abuse controls','Severity':'High','Status':'Blocked','Evidence':'P14-004; P9-002','Owner':'Engineering + Sales Ops','Exit_criteria':'Durable store, rate limit, duplicate policy, monitoring validated','Accepted_exception':'No'},
|
||||
{'Gate':'Native French and Arabic review','Severity':'High','Status':'Open','Evidence':'P9-005','Owner':'Localization + Content','Exit_criteria':'Native reviewers approve all visible and metadata strings','Accepted_exception':'No'},
|
||||
{'Gate':'Participant and accessibility acceptance research','Severity':'Critical','Status':'Blocked','Evidence':'P9-001','Owner':'Research + Product','Exit_criteria':'Required cohorts completed; S0/S1 findings fixed and retested','Accepted_exception':'No'},
|
||||
{'Gate':'Canonical domain, brand assets, and public destinations','Severity':'High','Status':'Blocked','Evidence':'P10-001; P10-002; destination registry','Owner':'Product + Infrastructure + Design','Exit_criteria':'Approved production origin, logo, legal, login, sales, support, tour, pricing destinations','Accepted_exception':'No'},
|
||||
{'Gate':'Final source-control commit identity','Severity':'Medium','Status':'Blocked','Evidence':'Supplied ZIP contains no .git metadata','Owner':'Engineering','Exit_criteria':'Commit final manifest and record immutable SHA','Accepted_exception':'No'},
|
||||
]
|
||||
csv_write('RELEASE_GATE_MATRIX_v1.0.csv', fields, gates)
|
||||
csv_write('RISK_ACCEPTANCE_REGISTER_v1.0.csv', ['ID','Gate','Risk','User_impact','Business_impact','Mitigation','Owner','Approver','Expiration','Required_follow_up'], [])
|
||||
|
||||
# Manual test matrix
|
||||
manual_fields = ['ID','Area','Platform_or_AT','Locale','Theme','Viewport_or_zoom','Status','Evidence','Blocking_issue']
|
||||
manual_rows = [
|
||||
{'ID':'M15-001','Area':'Keyboard-only primary demo flow','Platform_or_AT':'Desktop browser','Locale':'en/fr/ar','Theme':'light/dark','Viewport_or_zoom':'1440/390','Status':'Not run','Evidence':'Authored Playwright and component semantics only','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-002','Area':'NVDA flow','Platform_or_AT':'NVDA + Firefox/Chrome on Windows','Locale':'en/ar','Theme':'light/dark','Viewport_or_zoom':'desktop','Status':'Not run','Evidence':'None; human AT required','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-003','Area':'VoiceOver macOS','Platform_or_AT':'VoiceOver + Safari','Locale':'en/ar','Theme':'light/dark','Viewport_or_zoom':'desktop','Status':'Not run','Evidence':'None; human AT required','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-004','Area':'VoiceOver iOS','Platform_or_AT':'VoiceOver + Mobile Safari','Locale':'en/ar','Theme':'system','Viewport_or_zoom':'390-class device','Status':'Not run','Evidence':'None; physical/device lab required','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-005','Area':'TalkBack Android','Platform_or_AT':'TalkBack + Chrome','Locale':'en/ar','Theme':'system','Viewport_or_zoom':'390-class device','Status':'Not run','Evidence':'None; device/emulator required','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-006','Area':'200% and 400% zoom/reflow','Platform_or_AT':'Desktop browsers','Locale':'en/fr/ar','Theme':'light/dark','Viewport_or_zoom':'200%/400%','Status':'Not run','Evidence':'Responsive source and tests authored only','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-007','Area':'Forced colors and reduced motion','Platform_or_AT':'Windows forced colors; reduced-motion media','Locale':'en/fr/ar','Theme':'all','Viewport_or_zoom':'desktop/mobile','Status':'Not run','Evidence':'CSS controls present; human review absent','Blocking_issue':'P15-005'},
|
||||
{'ID':'M15-008','Area':'Arabic reading and focus order','Platform_or_AT':'Browser + Arabic screen reader review','Locale':'ar','Theme':'light/dark','Viewport_or_zoom':'1440/390/320','Status':'Not run','Evidence':'Logical CSS validator passes; human review absent','Blocking_issue':'P15-005'},
|
||||
]
|
||||
csv_write('MANUAL_TEST_MATRIX_v1.0.csv', manual_fields, manual_rows)
|
||||
|
||||
# WCAG matrix
|
||||
wcag_fields = ['Criterion','Level','Applicability','Status','Evidence','Remaining_validation','Related_issue']
|
||||
criteria = [
|
||||
('1.1.1 Non-text Content','A','Applicable','Static evidence only','MediaFrame requires alt; icon primitives support accessible names','Browser and screen-reader inspection of every rendered media state','P15-005'),
|
||||
('1.3.1 Info and Relationships','A','Applicable','Static evidence only','Semantic landmarks, labels, fieldsets, headings, native dialog','Manual screen-reader verification','P15-005'),
|
||||
('1.3.2 Meaningful Sequence','A','Applicable','Static evidence only','DOM order and logical CSS reviewed','EN/FR/AR screen-reader and keyboard order','P15-005'),
|
||||
('1.3.3 Sensory Characteristics','A','Applicable','Static evidence only','Instructions do not rely solely on position','Rendered content review','P15-005'),
|
||||
('1.4.1 Use of Color','A','Applicable','Static evidence only','Status text/icons accompany semantic color tokens','Manual light/dark/forced-colors review','P15-005'),
|
||||
('1.4.3 Contrast Minimum','AA','Applicable','Blocked','Token intent exists','Measured contrast in all states/themes required','P15-005'),
|
||||
('1.4.4 Resize Text','AA','Applicable','Blocked','Responsive typography authored','200% browser verification required','P15-005'),
|
||||
('1.4.10 Reflow','AA','Applicable','Blocked','320px responsive scenarios authored','400% and narrow viewport execution required','P15-005'),
|
||||
('1.4.11 Non-text Contrast','AA','Applicable','Blocked','Focus and border tokens exist','Rendered measurement required','P15-005'),
|
||||
('1.4.12 Text Spacing','AA','Applicable','Blocked','Content-driven dimensions used','Text-spacing override test required','P15-005'),
|
||||
('1.4.13 Content on Hover or Focus','AA','Applicable','Static evidence only','Tooltip component defined','Keyboard/pointer dismissal and persistence review','P15-005'),
|
||||
('2.1.1 Keyboard','A','Applicable','Blocked','Native controls and keyboard tests authored','Full keyboard-only flow required','P15-005'),
|
||||
('2.1.2 No Keyboard Trap','A','Applicable','Blocked','Native dialog used','Manual modal/drawer trap and escape review','P15-005'),
|
||||
('2.2.1 Timing Adjustable','A','Limited','Static evidence only','No user-facing session timer; bounded server timeout preserves form','Slow completion and retry review','P15-005'),
|
||||
('2.2.2 Pause Stop Hide','A','Applicable','Static evidence only','No autoplay media; reduced-motion styles exist','Rendered animation review','P15-005'),
|
||||
('2.3.1 Three Flashes','A','Applicable','Static evidence only','No flashing content identified in source','Visual confirmation','P15-005'),
|
||||
('2.4.1 Bypass Blocks','A','Applicable','Static evidence only','Skip navigation validator and shell tests authored','Keyboard confirmation','P15-005'),
|
||||
('2.4.2 Page Titled','A','Applicable','Static evidence only','Localized metadata builder validator passes','Rendered route title verification','P15-005'),
|
||||
('2.4.3 Focus Order','A','Applicable','Blocked','Logical DOM order and native controls','Manual all-state focus order','P15-005'),
|
||||
('2.4.4 Link Purpose','A','Applicable','Static evidence only','Destination registry and localized labels','Rendered route review','P15-005'),
|
||||
('2.4.6 Headings and Labels','AA','Applicable','Static evidence only','Structured headings and explicit form labels','Screen-reader review','P15-005'),
|
||||
('2.4.7 Focus Visible','AA','Applicable','Blocked','Focus tokens/styles exist','Rendered keyboard review in themes/forced colors','P15-005'),
|
||||
('2.4.11 Focus Not Obscured Minimum','AA','Applicable','Blocked','Sticky shell and dialog CSS reviewed','Viewport/zoom execution required','P15-005'),
|
||||
('2.5.7 Dragging Movements','AA','Applicable','Static evidence only','No drag-only control identified','Rendered interaction inventory confirmation','P15-005'),
|
||||
('2.5.8 Target Size Minimum','AA','Applicable','Blocked','Component sizing tokens exist','Measured desktop/mobile targets required','P15-005'),
|
||||
('3.1.1 Language of Page','A','Applicable','Static evidence only','Server-rendered lang and dir validators pass','Rendered HTML verification','P15-005'),
|
||||
('3.1.2 Language of Parts','AA','Applicable','Static evidence only','Bidi and localized content primitives exist','Foreign-language option pronunciation review','P15-005'),
|
||||
('3.2.1 On Focus','A','Applicable','Static evidence only','No focus-triggered navigation identified','Manual interaction review','P15-005'),
|
||||
('3.2.2 On Input','A','Applicable','Static evidence only','Locale/theme actions are explicit controls','Manual form and selector review','P15-005'),
|
||||
('3.2.3 Consistent Navigation','AA','Applicable','Static evidence only','Shared shell components','Cross-route rendered comparison','P15-005'),
|
||||
('3.3.1 Error Identification','A','Applicable','Static evidence only','Error summary, field errors, aria-invalid','Screen-reader announcement review','P15-005'),
|
||||
('3.3.2 Labels or Instructions','A','Applicable','Static evidence only','Required/optional labels and supporting text','Rendered/AT review','P15-005'),
|
||||
('3.3.3 Error Suggestion','AA','Applicable','Static evidence only','Localized actionable validation messages','Manual flow review','P15-005'),
|
||||
('3.3.4 Error Prevention','AA','Applicable','Static evidence only','No irreversible legal/financial transaction; duplicate controls present','Business policy review','P14-004'),
|
||||
('4.1.2 Name Role Value','A','Applicable','Static evidence only','Native controls and explicit labels; ARIA in composites','Automated and screen-reader execution','P15-005'),
|
||||
('4.1.3 Status Messages','AA','Applicable','Static evidence only','LiveRegion, role=alert, success aria-live','Actual announcement capture required','P15-005'),
|
||||
]
|
||||
csv_write('WCAG_2_2_AA_EVIDENCE_MATRIX_v1.0.csv', wcag_fields, [dict(zip(wcag_fields,row)) for row in criteria])
|
||||
|
||||
# Individual reports
|
||||
write('CLEAN_ROOM_BASELINE_REPORT_v1.0.md', report(
|
||||
'Phase 15 Clean-room Baseline Report','Blocked / failed acceptance',
|
||||
'The supplied ZIP was extracted into a fresh directory and its Phase 14 manifest verified 466 files. The exact Phase 15 clean install could not begin because the host does not provide the pinned Node/pnpm toolchain and cannot resolve the package registry.',
|
||||
['Fresh archive manifest passed (C15-001).','ZIP SHA-256 and lockfile SHA-256 are recorded in the implementation report.','No dependency or lockfile update was performed.'],
|
||||
['Node 24.17.0 is not installed; observed Node is 22.16.0.','pnpm 11.9.0 activation failed because registry DNS is unavailable.','Frozen install and every dependency-backed command are blocked.'],
|
||||
'This gate fails Phase 15 acceptance. A connected clean runner using the pinned versions must rerun the complete matrix.'
|
||||
))
|
||||
write('REPOSITORY_INTEGRITY_AUDIT_v1.0.md', report(
|
||||
'Phase 15 Repository Integrity Audit','Passed for static package integrity',
|
||||
'The baseline manifest passed before changes. The Phase 15 audit checks JSON/CSV structure, symlinks, generated files, secret patterns, reserved test email domains, runtime URLs, dependency pinning, and release-control invariants.',
|
||||
['Baseline manifest: 466 files verified.','Baseline audit exposed 4 High and 7 Medium findings.','Current audit reports zero findings under the same controls.'],
|
||||
['Final package manifest validation occurs after all reports are generated.','Git commit history was not supplied and cannot be audited.'],
|
||||
'Static repository integrity is acceptable, but it does not replace build, dependency, browser, or human QA evidence.'
|
||||
))
|
||||
write('STATIC_ANALYSIS_REPORT_v1.0.md', report(
|
||||
'Phase 15 Static Analysis Report','Partially passed',
|
||||
'All dependency-free repository validators and the Phase 15 audit pass after four defects were repaired. Package-backed formatting, ESLint, strict TypeScript, dead-code tooling, dependency/license analysis, and client-bundle inspection could not run on the final tree.',
|
||||
['16 repository validators pass.','33 JavaScript and 64 TypeScript modules plus two Python scripts pass syntax checks.','JSON/CSV, secret, URL, PII-fixture, direct-dependency, visual-control, and CI-control audit passes.'],
|
||||
['Prettier check, ESLint, strict typecheck, dead-code and unused-dependency tools.','Registry-backed audit and license validation.','Production client-bundle secret inspection.'],
|
||||
'Static source controls pass; the overall static-analysis gate remains blocked until package-backed checks run.'
|
||||
))
|
||||
write('UNIT_TEST_REPORT_v1.0.md', report(
|
||||
'Phase 15 Unit Test Report','Blocked for current release candidate',
|
||||
'A Vitest regression test was added for exact JSON media-type handling. The current unit suite could not execute because dependencies are unavailable. Phase 14 recorded 39 passing unit tests, but that result predates Phase 15 code changes.',
|
||||
['Eight dependency-free assertions validate the new media-type helper.','Phase 14 historical result: 39 passed, 0 failed, with recorded coverage.'],
|
||||
['Current `pnpm test:unit` and coverage thresholds.','Current React/component and Zod-dependent tests.'],
|
||||
'Do not use the Phase 14 unit result as current release evidence. Rerun on the final manifest.'
|
||||
))
|
||||
write('INTEGRATION_TEST_REPORT_v1.0.md', report(
|
||||
'Phase 15 Integration Test Report','Blocked for current release candidate',
|
||||
'Phase 14 recorded four passing demo-service integration tests. Phase 15 discovered that CI did not execute this suite and corrected the workflow, but the final suite could not run locally.',
|
||||
['CI now contains `pnpm test:integration`.','Static validator confirms the integration gate remains configured.','Historical Phase 14 integration tests cover success, idempotent transport, normalized failure, and timeout.'],
|
||||
['Current Vitest integration execution.','Production/sandbox CRM, scheduler, email, and durable idempotency integrations do not exist.'],
|
||||
'The integration gate is blocked until connected CI executes the suite and approved vendor adapters exist.'
|
||||
))
|
||||
write('END_TO_END_TEST_REPORT_v1.0.md', report(
|
||||
'Phase 15 End-to-End Test Report','Blocked',
|
||||
'The repository contains authored Playwright flows for locale pages, navigation, the demo workflow, responsive states, no-JavaScript behavior, accessibility, and visual checks. No current Phase 15 browser flow executed.',
|
||||
['Scenario source was inspected.','CI now invokes browser, accessibility, and visual slices explicitly.'],
|
||||
['All 50 required Phase 15 end-to-end scenarios.','Success, validation, timeout, duplicate, history, offline, and dependency-failure browser evidence.'],
|
||||
'High release blocker P15-005 remains open.'
|
||||
))
|
||||
write('CROSS_BROWSER_TEST_REPORT_v1.0.md', report(
|
||||
'Phase 15 Cross-browser Test Report','Blocked',
|
||||
'The configured matrix includes Chromium, Firefox, WebKit, mobile Chrome, and mobile Safari. No current application run was possible.',
|
||||
['Playwright project configuration statically reviewed.','Canonical visual snapshots are now Chromium-only; other engines remain functional targets.'],
|
||||
['Browser installation in the pinned environment.','Functional results by browser/version/OS/device/locale/theme/viewport.'],
|
||||
'No browser support claim is approved from configuration alone.'
|
||||
))
|
||||
write('DEVICE_VIEWPORT_REPORT_v1.0.md', report(
|
||||
'Phase 15 Device and Viewport Report','Blocked for rendered validation',
|
||||
'Responsive CSS and authored tests cover the required widths, but no final rendered matrix was observed.',
|
||||
['Logical CSS validator passes across 47 CSS files.','Authored scenarios include 1440, 1024, 768, 390, and 320 widths.'],
|
||||
['Horizontal overflow, clipping, focus visibility, touch targets, safe areas, reduced height, and mobile landscape checks.'],
|
||||
'Responsive implementation remains unaccepted until browser evidence is attached.'
|
||||
))
|
||||
write('ZOOM_REFLOW_REPORT_v1.0.md', report(
|
||||
'Phase 15 Zoom and Reflow Report','Blocked',
|
||||
'Source uses content-driven dimensions and logical properties, and text-expansion scenarios exist. Actual 200%/400% zoom, OS text scaling, French expansion, Arabic expansion, and pseudolocalization were not run.',
|
||||
['Static logical CSS checks pass.','Manual matrix identifies required zoom and AT combinations.'],
|
||||
['200% and 400% browser zoom.','Text-spacing overrides and OS scaling.','Dialog/error-summary focus visibility at zoom.'],
|
||||
'WCAG reflow and resize criteria are not verified.'
|
||||
))
|
||||
write('AUTOMATED_ACCESSIBILITY_REPORT_v1.0.md', report(
|
||||
'Phase 15 Automated Accessibility Report','Blocked',
|
||||
'Axe suites are authored for shell, homepage, component system, and demo workflow. They could not execute on the final Phase 15 tree.',
|
||||
['A11y source scenarios inspected.','Static semantic controls and WCAG matrix prepared.'],
|
||||
['Axe results for EN/FR/AR, themes, mobile, form states, dialogs, FAQ, scheduler, forced colors.'],
|
||||
'No automated accessibility pass is claimed.'
|
||||
))
|
||||
write('KEYBOARD_ACCESSIBILITY_REPORT_v1.0.md', report(
|
||||
'Phase 15 Keyboard Accessibility Report','Not run',
|
||||
'Native controls, native dialog, focus-return references, skip navigation, form error links, and visible-focus styles are present in source. Keyboard-only behavior was not manually observed.',
|
||||
['Source-level focus and semantic review.','Manual test matrix created.'],
|
||||
['Complete pointer-free path including mobile navigation, form errors, consent, dialogs, success, and recovery.'],
|
||||
'Manual keyboard acceptance is a High blocker.'
|
||||
))
|
||||
write('SCREEN_READER_REPORT_v1.0.md', report(
|
||||
'Phase 15 Screen-reader Report','Not run',
|
||||
'No NVDA, VoiceOver, or TalkBack session was performed. The report intentionally contains no invented announcements.',
|
||||
['Screen-reader combinations and required flows are listed in MANUAL_TEST_MATRIX_v1.0.csv.','Static language, direction, labels, status regions, and dialog relationships reviewed.'],
|
||||
['Actual announcement transcripts for English and Arabic.','Focus movement, error/status announcements, mixed-direction values, and mobile AT behavior.'],
|
||||
'Screen-reader acceptance is blocked.'
|
||||
))
|
||||
write('LOCALIZATION_QA_REPORT_v1.0.md', report(
|
||||
'Phase 15 Localization QA Report','Static parity passed; linguistic approval blocked',
|
||||
'English, French, and Arabic catalogs have exact key parity and valid direction metadata. Native linguistic review remains open.',
|
||||
['Locale validator passes.','Metadata/route parity validator passes.','No non-reserved personal-data fixture or runtime implementation key was found by the audit.'],
|
||||
['Native French review.','Native formal Modern Standard Arabic review.','External vendor localization because vendors are not selected.'],
|
||||
'Mechanical parity is not translation approval; P9-005 remains High.'
|
||||
))
|
||||
write('RTL_QA_REPORT_v1.0.md', report(
|
||||
'Phase 15 RTL QA Report','Static controls passed; end-to-end review blocked',
|
||||
'The Arabic locale is configured for server-rendered RTL and the CSS validator reports 47 files using logical declarations. Native visual, focus-order, screen-reader, mixed-direction, and scheduler checks did not run.',
|
||||
['Locale direction metadata passes.','Logical CSS validation passes.','Bidi primitives and LTR treatment for email/tel/url values are present.'],
|
||||
['Rendered header, hero, workflow, forms, error summaries, dialogs, footer, animation, and 320px overflow review.'],
|
||||
'Arabic RTL is not accepted from text alignment alone.'
|
||||
))
|
||||
write('THEME_QA_REPORT_v1.0.md', report(
|
||||
'Phase 15 Theme QA Report','Static controls passed; browser verification blocked',
|
||||
'Light, dark, and system foundations, nonce pre-paint bootstrap, persistence, and CSS fallback are present. A stale visual-test storage key was fixed.',
|
||||
['Theme validators pass.','Canonical visual setup now uses `hpc.theme.preference`.','Semantic token controls pass.'],
|
||||
['No-flash behavior, OS theme changes, delayed JavaScript, disabled JavaScript, contrast, embeds, and forced-colors states.'],
|
||||
'Theme acceptance remains blocked pending rendered evidence.'
|
||||
))
|
||||
write('VISUAL_REGRESSION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Visual Regression Report','Blocked',
|
||||
'No approved PNG baselines exist. Phase 15 corrected snapshot determinism but did not generate or approve product images.',
|
||||
['Snapshot suites now use canonical Chromium scope.','Component-system test now writes the actual theme storage key.'],
|
||||
['Baseline generation in pinned Chromium.','Manual diff classification for locales, themes, viewports, interaction states, focus, reduced motion, forced colors.'],
|
||||
'Updating baselines is prohibited until the application executes and reviewers inspect every diff.'
|
||||
))
|
||||
write('DESIGN_FIDELITY_REPORT_v1.0.md', report(
|
||||
'Phase 15 Design Fidelity Report','Blocked for visual comparison',
|
||||
'Section order and component composition validators pass, but no rendered comparison against Phase 7 production designs was performed.',
|
||||
['Homepage composition and content-model validators pass.','Design-token immutability and semantic color controls pass.'],
|
||||
['Typography, spacing, alignment, media scale, responsive adaptation, themes, RTL, focus, loading, error, and success visual comparison.'],
|
||||
'Design fidelity is unverified, not failed.'
|
||||
))
|
||||
write('PERFORMANCE_REPORT_v1.0.md', report(
|
||||
'Phase 15 Performance Report','Blocked',
|
||||
'Approved budgets exist for LCP, INP, CLS, TTFB, transfer sizes, fonts, media, and third-party requests. No final production build or browser timing was available.',
|
||||
['Budget contract inspected: LCP 2500 ms, INP 200 ms, CLS 0.1, TTFB 800 ms maximums.','No approved vendor SDK or third-party runtime was added.'],
|
||||
['Cold/warm desktop/mobile measurements in EN and AR.','LCP asset, main-thread, hydration, form latency, scheduler latency, failure response, and third-party request measurement.'],
|
||||
'Performance gate is blocked; historical Phase 14 build inventory is not a Phase 15 measurement.'
|
||||
))
|
||||
write('BUNDLE_ANALYSIS_REPORT_v1.0.md', report(
|
||||
'Phase 15 Bundle Analysis Report','Blocked for final build',
|
||||
'Source review confirms no new runtime dependency was added and the content-type helper is server-side. Final route chunks and client boundaries could not be measured.',
|
||||
['Direct dependency pins and lockfile contract pass.','No vendor SDK, consent manager, scheduler, email, or analytics network package is present.'],
|
||||
['Production route bundle sizes, duplicate packages/locales/icons, source maps, tree shaking, dynamic imports, and client secret inspection.'],
|
||||
'Bundle approval requires the final production build.'
|
||||
))
|
||||
write('IMAGE_FONT_PERFORMANCE_REPORT_v1.0.md', report(
|
||||
'Phase 15 Image and Font Performance Report','Blocked for rendered/network measurement',
|
||||
'The public directory contains no production visual assets. Inter and Noto Sans Arabic remain package dependencies. No network waterfall or font-layout measurement ran.',
|
||||
['Asset inventory confirms no unapproved logo, screenshot, hero image, or social image was added.','Arabic font is separately represented in dependency and token foundations.'],
|
||||
['Final logo/social/hero assets, responsive sizes, compression, preload policy, Arabic font payload, CLS, cache headers.'],
|
||||
'Final asset approval and performance measurement remain Phase 16 gates after QA rerun.'
|
||||
))
|
||||
write('SECURITY_VALIDATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Security Validation Report','Static controls passed; dynamic and dependency checks blocked',
|
||||
'Static review confirms request marker, same-origin check, exact JSON content type, body limit, strict schema, honeypot, bounded timeout, idempotency, normalized errors, no-store responses, CSP, and server-only environment controls. The prefix media-type defect was fixed.',
|
||||
['Phase 14 security validator passes through C15-004.','Secret-pattern and runtime URL audit passes.','Exact content-type behavior assertions pass.'],
|
||||
['Registry vulnerability/license audit.','Dynamic CSRF, origin, replay, malformed input, CSP, headers, rate limit, webhook, and vendor credential testing on final build.'],
|
||||
'Security is not production-approved. Rate limiting, durable idempotency, vendors, and dynamic evidence remain blockers.'
|
||||
))
|
||||
write('PRIVACY_VALIDATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Privacy Validation Report','Static controls passed; legal and runtime approval blocked',
|
||||
'Static checks find no personal data in analytics contracts, URLs, runtime logs, fixtures outside reserved domains, or browser persistence paths for form values. No production processing destination exists.',
|
||||
['Privacy validator passes.','Audit finds only reserved `.test`/`.invalid` email fixtures.','Analytics context remains strict and excludes form fields.'],
|
||||
['Runtime browser capture, logs, monitoring, vendor payloads, retention, deletion, recipients, transfers, staging separation.','Legal/privacy approval.'],
|
||||
'P14-002 and related legal gates block production.'
|
||||
))
|
||||
write('CONSENT_VALIDATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Consent Validation Report','Blocked by product/legal decisions',
|
||||
'Analytics is disabled by default and optional marketing consent is not implemented. This prevents premature vendor loading but does not constitute a complete consent system.',
|
||||
['Static environment defaults prohibit production analytics.','Test analytics requires an explicit test-only document flag.','Consent matrix reviewed.'],
|
||||
['No-choice, necessary-only, accept/reject, withdrawal, returning visitor, locale change, consent version, vendor failure, delayed JavaScript.','Localized approved legal language and storage mechanism.'],
|
||||
'No consent-compliance claim is permitted.'
|
||||
))
|
||||
write('ANALYTICS_VALIDATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Analytics Validation Report','Static contract passed; provider/runtime blocked',
|
||||
'The allowlisted event schema permits only route, locale, direction, resolved theme, source, step, error code/count, and reason values. The current sink has no network and requires test consent.',
|
||||
['Analytics/privacy validators pass.','Forbidden payload keys include name, email, company, message, phone, field value, and server response.'],
|
||||
['Provider, consent gating in real browser, duplicate hydration events, route changes, captured event evidence, retention, region, DPA.'],
|
||||
'Analytics must remain disabled.'
|
||||
))
|
||||
write('DESTINATION_BROKEN_LINK_REPORT_v1.0.md', report(
|
||||
'Phase 15 Destination and Broken-link Report','Static registry passed; external destinations blocked',
|
||||
'The route and destination validators pass. Demo is local-only; contact sales, product tour, login, cookie settings, and support are blocked; pricing/legal/accessibility routes are placeholders.',
|
||||
['Five route IDs have complete locale/hash/query/hreflang contracts.','No unapproved runtime external URL was found.','Destination registry is explicit.'],
|
||||
['Rendered broken-link crawl of generated routes.','Production URLs, redirects, status codes, canonical/hreflang, external security behavior.'],
|
||||
'Blocked destinations must not be activated until approved.'
|
||||
))
|
||||
write('FAILURE_SIMULATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Failure Simulation Report','Blocked for current execution',
|
||||
'Deterministic scenarios exist for success, duplicate, timeout, integration error, validation, and blocked configuration. Current browser/server simulations did not run.',
|
||||
['Source adapters and scenario guards reviewed.','Test scenarios remain disabled in production.'],
|
||||
['CRM/scheduler/analytics/consent/email/DNS/network/slow/partial/rate-limit/storage/script/CSP/offline failures.'],
|
||||
'Authored scenarios are not observed resilience evidence.'
|
||||
))
|
||||
write('IDEMPOTENCY_DUPLICATE_REPORT_v1.0.md', report(
|
||||
'Phase 15 Idempotency and Duplicate Report','Local static design reviewed; production gate blocked',
|
||||
'The current process-local store coalesces repeated keys, caches non-retryable results, removes retryable failures, and contains no personal data in keys. It is explicitly test/local only.',
|
||||
['Historical Phase 14 integration test covered repeated transport.','Source review confirms retryable failures release the key.'],
|
||||
['Current integration/browser retry execution.','Distributed durable store, late vendor success, cross-instance duplicate policy, legitimate later request policy.'],
|
||||
'P14-004 blocks scaled production.'
|
||||
))
|
||||
write('ABUSE_CONTROL_REPORT_v1.0.md', report(
|
||||
'Phase 15 Abuse-control Report','Partial static controls; production gate blocked',
|
||||
'The API enforces a 16 KiB body limit, request marker, origin check, strict schema, content type, and honeypot. No distributed rate limit or approved CAPTCHA/accessibility alternative exists.',
|
||||
['Static API controls pass.','Exact content type defect fixed.'],
|
||||
['Rapid requests, oversized/malformed payloads, origins, scripted submissions, slow assistive-technology users, international formats, threshold behavior.','Distributed rate limiting and operational monitoring.'],
|
||||
'Production submission remains blocked.'
|
||||
))
|
||||
write('DEPLOYMENT_REHEARSAL_REPORT_v1.0.md', report(
|
||||
'Phase 15 Deployment Rehearsal Report','Not run',
|
||||
'No production-like deployment was built or started from the Phase 15 manifest. Historical Phase 14 standalone HTTP evidence is retained but stale after code and CI changes.',
|
||||
['Dockerfile and CI deployment build path statically reviewed.','Production environment defaults fail closed.'],
|
||||
['Clean build, assets, headers, redirects, metadata, robots, health/startup failure, flags, observability, no test adapter, exact commands and timing.'],
|
||||
'Deployment gate blocks release.'
|
||||
))
|
||||
write('ROLLBACK_REHEARSAL_REPORT_v1.0.md', report(
|
||||
'Phase 15 Rollback Rehearsal Report','Documented concept; not rehearsed',
|
||||
'The public demo and analytics can be disabled through existing flags, and production defaults are blocked. No platform rollback was executed.',
|
||||
['Feature-flag inventory and Phase 14 rollback guidance reviewed.'],
|
||||
['Restore previous release, disable CRM/scheduler/analytics independently, verify no resend, stale secret handling, fallback UX, timing, owner confirmation.'],
|
||||
'Rollback readiness is unproven.'
|
||||
))
|
||||
write('OBSERVABILITY_VALIDATION_REPORT_v1.0.md', report(
|
||||
'Phase 15 Observability Validation Report','Blocked',
|
||||
'Correlation identifiers are non-sensitive and raw form logging is absent. No production logging, metrics, dashboards, alert thresholds, retention, or environment separation is implemented.',
|
||||
['Static no-raw-log controls pass.','Redaction helper and correlation IDs reviewed.'],
|
||||
['Submission/failure/vendor latency/rate-limit/duplicate/config/client/server/build/deployment metrics and alerts.'],
|
||||
'P14-005 blocks production operations.'
|
||||
))
|
||||
write('CONTENT_EVIDENCE_REVIEW_v1.0.md', report(
|
||||
'Phase 15 Content and Evidence Review','Static prohibited-content controls passed; approvals blocked',
|
||||
'Runtime validation finds no TODO/TBD/lorem, invented metrics, fake quotes, guessed external destinations, or research-only backdoor. Public claims and assets still require accountable approval.',
|
||||
['Prohibited-content validator passes.','No production image/logo/customer evidence asset is present.','Pending legal and unsupported destinations remain gated.'],
|
||||
['Native linguistic approval, claims, customer logos/testimonials, security/compliance statements, pricing, screenshots, contact details, legal content.'],
|
||||
'Content is structurally safe but not final-approved.'
|
||||
))
|
||||
write('SEO_METADATA_REVIEW_v1.0.md', report(
|
||||
'Phase 15 SEO and Metadata Review','Static foundation passed; rendered validation blocked',
|
||||
'Localized metadata, canonical, hreflang, Open Graph, Twitter, robots, and non-indexable placeholder controls pass static validation.',
|
||||
['Metadata validator passes.','Five route IDs have complete locale alternates.','Public release defaults to noindex.'],
|
||||
['Production origin, rendered status codes/redirects, sitemap if implemented, social assets, final metadata copy, crawl.'],
|
||||
'SEO release approval awaits production domain, assets, build, and crawl evidence.'
|
||||
))
|
||||
write('BROWSER_HISTORY_REPORT_v1.0.md', report(
|
||||
'Phase 15 Browser History and Navigation Report','Blocked',
|
||||
'Source avoids form values in URLs and keeps the demo in a dialog on the localized homepage. Direct entry, locale/theme changes, back/forward, refresh, success, resubmit, deep links, and external return were not executed.',
|
||||
['Static privacy audit finds no form-value URL construction.','Authored browser scenarios include success URL checks.'],
|
||||
['Complete back/forward/refresh/modal/scheduler/hash/redirect history matrix and focus restoration.'],
|
||||
'History behavior is unverified on the final candidate.'
|
||||
))
|
||||
write('RETEST_REPORT_v1.0.md', report(
|
||||
'Phase 15 Retest Report','Static retest passed; full regression blocked',
|
||||
'All four remediated Phase 15 defects pass targeted static or dependency-free retests. Browser and package-backed nearby regressions were not executable.',
|
||||
['P15-001: CSV structure passes.','P15-002: exact media-type assertions and validator pass.','P15-003: canonical theme key and Chromium scope validator pass.','P15-004: CI integration command validator passes.'],
|
||||
['Prettier/ESLint/typecheck/unit/integration/build.','Browser visual/theme/form regression.'],
|
||||
'Resolved defects remain conditionally closed until the final connected CI run.'
|
||||
))
|
||||
|
||||
# Final recommendation
|
||||
recommendation = f'''# Phase 15 Final Release Recommendation
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Not ready for Phase 16**
|
||||
|
||||
The repository has materially better static controls than the supplied Phase 14 baseline, but final QA is not complete. The exact clean installation, production build, dependency audit, unit/integration regression, cross-browser flows, automated accessibility, manual keyboard/screen-reader/zoom review, visual baselines, performance budgets, deployment rehearsal, and rollback rehearsal have not been executed against the final Phase 15 manifest.
|
||||
|
||||
## Defect and issue counts
|
||||
|
||||
- Critical unresolved: **{unresolved_counts.get('Critical',0)}**
|
||||
- High unresolved: **{unresolved_counts.get('High',0)}**
|
||||
- Medium unresolved: **{unresolved_counts.get('Medium',0)}**
|
||||
- Low unresolved: **{unresolved_counts.get('Low',0)}**
|
||||
- Phase 15 defects resolved: **4**
|
||||
- Accepted exceptions: **0**
|
||||
|
||||
The cumulative counts include inherited business, research, legal, localization, design, integration, operations, and verification issues. The Phase 15 defect register contains one open High aggregate acceptance blocker, P15-005.
|
||||
|
||||
## Failed or blocked release gates
|
||||
|
||||
The blocked gates include exact runtime/frozen install, final package-backed checks, current unit/integration/build, dependency/license audit, browser/accessibility/visual/manual matrices, performance and bundle measurement, production-like deployment, rollback, CRM, legal/privacy consent, analytics approval, distributed idempotency/rate limiting, native localization review, participant acceptance research, production origin, final brand assets, destinations, and a real commit identifier.
|
||||
|
||||
## Required Phase 16 approvals
|
||||
|
||||
Phase 16 must not begin release-candidate freeze until engineering QA closes P15-005. Business/legal work still required includes final copy, claims/evidence, customer assets, legal/privacy text, analytics and consent, CRM/scheduler operations, production destinations, domain/hosting, credentials, launch ownership, and final assets.
|
||||
|
||||
## Required remediation before production
|
||||
|
||||
1. Commit the Phase 15 repository and record the SHA.
|
||||
2. Run the complete command matrix on Node 24.17.0 and pnpm 11.9.0 from a frozen clean install.
|
||||
3. Fix and retest every resulting Critical, High, and Medium defect.
|
||||
4. Complete browser, accessibility, localization/RTL, theme, responsive, visual, performance, deployment, rollback, and observability evidence.
|
||||
5. Resolve or formally approve every production business/legal gate. No Critical or High exception currently exists.
|
||||
|
||||
## Overall risk statement
|
||||
|
||||
Static source integrity is improved and production integrations remain safely disabled. Release risk is nevertheless **unacceptably high** because the final executable artifact has not been built or observed in browsers and assistive technologies, and the production data-processing and destination decisions are unresolved.
|
||||
'''
|
||||
write('FINAL_RELEASE_RECOMMENDATION_v1.0.md', recommendation)
|
||||
|
||||
# Risk register note
|
||||
write('RISK_ACCEPTANCE_SUMMARY_v1.0.md', '# Phase 15 Risk Acceptance Summary\n\nNo accepted exceptions were created. `RISK_ACCEPTANCE_REGISTER_v1.0.csv` intentionally contains only its header.\n')
|
||||
|
||||
# Phase 15 implementation report
|
||||
sections = [
|
||||
('1. Executive summary','Phase 15 found and fixed four repository defects: corrupted release-evidence CSV rows, loose JSON media-type matching, nondeterministic visual-test setup, and missing CI integration coverage. Dependency-free validation passes. Final acceptance is blocked because the pinned clean install and all dependency-backed/browser/human/production-like gates could not run.'),
|
||||
('2. Authoritative inputs reviewed','The Phase 15 prompt, complete Phase 14 repository, Phase 14 handoff, implementation report, decision log, issue register, manifest, route/destination/form/integration/data-flow/consent/analytics/security/privacy/performance/release-gate records, tests, CI, README, and inherited Phase 9 through Phase 13 contracts were reviewed.'),
|
||||
('3. Test environment',f"Observed: Debian GNU/Linux 13, Node {get_version(['node','--version'])}, npm {get_version(['npm','--version'])}, Corepack {get_version(['corepack','--version'])}, Python {platform.python_version()}, Chromium 144.0.7559.96. Required: Node 24.17.0 and pnpm 11.9.0. The supplied ZIP contained no `.git` directory."),
|
||||
('4. Clean-room baseline','Fresh extraction and Phase 14 manifest verification passed. Corepack could not fetch pnpm because registry DNS is unavailable; frozen install did not run.'),
|
||||
('5. Repository integrity','Baseline static audit found 4 High and 7 Medium findings. After remediation, the same audit reports zero findings. Final artifact integrity is governed by the Phase 15 manifest.'),
|
||||
('6. Static analysis','Sixteen dependency-free validators pass. JavaScript/non-JSX TypeScript syntax and the Python audit script pass. Prettier, ESLint, strict TypeScript, dead-code, dependency/license, and bundle-secret tooling are blocked.'),
|
||||
('7. Unit testing','Current Vitest suite not run. A media-type regression test was authored and eight dependency-free assertions pass. Historical Phase 14 unit evidence is stale for this release candidate.'),
|
||||
('8. Integration testing','Current integration suite not run. CI omission was fixed. Historical Phase 14 integration evidence is stale.'),
|
||||
('9. End-to-end testing','Authored scenarios exist; no current browser execution.'),
|
||||
('10. Cross-browser testing','Configured projects cover Chromium, Firefox, WebKit, mobile Chrome, and mobile Safari; no current results.'),
|
||||
('11. Device and viewport testing','Required widths are represented in tests; rendered validation blocked.'),
|
||||
('12. Zoom and reflow','No current 200/400 percent or OS scaling evidence.'),
|
||||
('13. Automated accessibility','Axe suites authored; not executed.'),
|
||||
('14. Keyboard accessibility','Static semantics reviewed; manual keyboard flow not run.'),
|
||||
('15. Screen-reader testing','NVDA, VoiceOver, and TalkBack not run. No announcement transcript was invented.'),
|
||||
('16. WCAG 2.2 AA assessment','Criterion-to-evidence matrix produced. Many criteria have static evidence only; conformance is not established.'),
|
||||
('17. Localization QA','Catalog key parity passes. Native French and Arabic approval remains open.'),
|
||||
('18. RTL QA','Document direction and logical CSS controls pass; Arabic end-to-end and AT review blocked.'),
|
||||
('19. Theme QA','Theme foundations pass static checks; no-flash, system changes, contrast, and forced-colors browser evidence blocked. Visual key defect fixed.'),
|
||||
('20. Visual regression','Canonical Chromium setup fixed. No baselines generated or approved.'),
|
||||
('21. Design fidelity','Composition/token validators pass; rendered design comparison not performed.'),
|
||||
('22. Performance','Approved budgets exist; no final build or browser metrics.'),
|
||||
('23. Bundle analysis','No new runtime dependency added; final chunks and client boundaries not measured.'),
|
||||
('24. Image and font performance','No production public assets added; final images/fonts/network behavior unmeasured.'),
|
||||
('25. Security validation','Static request, schema, timeout, idempotency, CSP, redaction, and fail-closed controls pass. Content-type boundary hardened. Dynamic/dependency/vendor/rate-limit evidence blocked.'),
|
||||
('26. Privacy validation','Static no-PII controls pass. Legal basis, recipients, retention, deletion, transfers, and runtime vendor/log evidence blocked.'),
|
||||
('27. Consent validation','Optional processing remains disabled; complete consent lifecycle not implemented or tested.'),
|
||||
('28. Analytics validation','Strict non-PII allowlist passes static review; provider and real consent gating not approved.'),
|
||||
('29. Destination validation','Registry and routes pass static checks; most production destinations remain blocked or placeholder.'),
|
||||
('30. Failure simulation','Deterministic scenarios authored; current runtime execution blocked.'),
|
||||
('31. Idempotency and duplicate handling','Process-local design reviewed; durable cross-instance policy remains blocked.'),
|
||||
('32. Abuse-control validation','Body size, marker, origin, schema, content type, and honeypot controls present; distributed rate limits absent.'),
|
||||
('33. Deployment rehearsal','Not run for Phase 15.'),
|
||||
('34. Rollback rehearsal','Feature-flag strategy documented; not rehearsed.'),
|
||||
('35. Observability validation','Correlation/redaction foundations exist; monitoring, alerts, retention, and dashboards absent.'),
|
||||
('36. Content and evidence review','Prohibited-content controls pass; final claims, evidence, assets, copy, and legal approvals remain open.'),
|
||||
('37. SEO and metadata','Static foundations pass; production-domain and rendered crawl evidence blocked.'),
|
||||
('38. Browser history and navigation','No PII URL construction found; final browser history matrix not run.'),
|
||||
('39. Defect summary','Four defects fixed: two High and two Medium. One High Phase 15 acceptance blocker remains open.'),
|
||||
('40. Retest summary','Targeted static/dependency-free retests pass. Full regression is blocked.'),
|
||||
('41. Release-gate assessment','Three static gates pass. All executable, browser/human, performance, deployment, and production approval gates remain blocked/open.'),
|
||||
('42. Accepted exceptions','None.'),
|
||||
('43. Remaining risks',f"Cumulative unresolved register: Critical {unresolved_counts.get('Critical',0)}, High {unresolved_counts.get('High',0)}, Medium {unresolved_counts.get('Medium',0)}, Low {unresolved_counts.get('Low',0)}."),
|
||||
('44. Files added, changed, and removed','Source changes harden content type; tests and CI are corrected; Phase 15 validators, reports, registers, and manifest are added. No authoritative Phase 9 contract was modified. File inventory is in `docs/quality/PHASE_15_FILE_CHANGES_v1.0.txt`.'),
|
||||
('45. Exact commands required to reproduce validation','See `docs/phase15/COMMAND_MATRIX_v1.0.csv` and the Phase 16 handoff. Blocked commands must be run exactly on the pinned toolchain.'),
|
||||
('46. Final release recommendation','**Not ready for Phase 16.** See `docs/phase15/FINAL_RELEASE_RECOMMENDATION_v1.0.md`.'),
|
||||
('47. Phase 16 readiness assessment','Phase 16 may review blockers and prepare approvals, but must not freeze or deploy a release candidate until P15-005 and all Critical/High release gates are closed or explicitly approved by accountable owners. No exception is currently accepted.'),
|
||||
]
|
||||
impl = ['# Phase 15 Implementation Report','',f'**Date:** {DATE}','',*sum(([f'## {title}', '', body, ''] for title,body in sections),[])]
|
||||
(ROOT/'docs/PHASE_15_IMPLEMENTATION_REPORT_v1.0.md').write_text('\n'.join(impl).rstrip()+'\n',encoding='utf-8')
|
||||
|
||||
# Phase 16 handoff
|
||||
handoff = f'''# Phase 16 Handoff: Final Content Approval and Deployment Preparation
|
||||
|
||||
## Handoff status
|
||||
|
||||
Phase 15 recommendation: **Not ready for Phase 16**. This handoff exists to make the remaining work explicit, not to imply release approval.
|
||||
|
||||
## Artifact identity
|
||||
|
||||
- Supplied archive had no Git metadata; final validated commit identifier is **unavailable**.
|
||||
- Use `PHASE_15_PACKAGE_MANIFEST_v1.0.json` and the delivered ZIP checksum to identify this artifact.
|
||||
- Phase 16 must commit the exact package and record the immutable commit SHA before release-candidate freeze.
|
||||
|
||||
## Evidence package
|
||||
|
||||
Review `docs/PHASE_15_IMPLEMENTATION_REPORT_v1.0.md`, `docs/phase15/FINAL_RELEASE_RECOMMENDATION_v1.0.md`, `docs/phase15/DEFECT_REGISTER_v1.0.csv`, `docs/phase15/RELEASE_GATE_MATRIX_v1.0.csv`, `docs/phase15/RISK_ACCEPTANCE_REGISTER_v1.0.csv`, `docs/phase15/COMMAND_MATRIX_v1.0.csv`, and all Phase 15 reports.
|
||||
|
||||
## Production configuration inventory
|
||||
|
||||
Authoritative inventories remain `.env.example` and the Phase 14 environment, feature-flag, destination, external-service, data-flow, consent, and analytics registries. Production defaults remain blocked. No production credential is included.
|
||||
|
||||
## Required engineering validation before freeze
|
||||
|
||||
1. Run Node 24.17.0 and pnpm 11.9.0.
|
||||
2. Run `pnpm install --frozen-lockfile`.
|
||||
3. Run `pnpm validate`, `pnpm validate:package`, `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, `pnpm test:unit`, `pnpm test:integration`, `pnpm test:security`, `pnpm test:privacy`, and the production build.
|
||||
4. Install pinned Playwright browsers and run browser, accessibility, and visual suites.
|
||||
5. Complete manual keyboard, screen-reader, zoom/reflow, forced-colors, reduced-motion, Arabic RTL, and mobile device matrices.
|
||||
6. Measure performance and bundles against the approved budgets.
|
||||
7. Rehearse production-like deployment and rollback.
|
||||
8. Close all Critical/High defects and retest Medium defects.
|
||||
|
||||
## Required business and operational approvals
|
||||
|
||||
- Final EN/FR/AR copy and native linguistic review
|
||||
- Claims, evidence, customer logos, testimonials, and product captures
|
||||
- Legal/privacy/consent text, legal basis, retention, deletion, recipients, and transfers
|
||||
- Analytics provider, event approval, consent category, DPA, region, retention, and owner
|
||||
- CRM lead destination, mapping, authentication, duplicate/retry policy, sandbox, monitoring, and owner
|
||||
- Scheduler and email decisions if enabled
|
||||
- Production origin, DNS, hosting, cache/security headers, redirects, robots, canonical, and hreflang
|
||||
- Final logo, social assets, hero/product assets, and performance approval
|
||||
- Production secrets through an approved secret manager with least privilege and rotation
|
||||
- Launch owner matrix, launch-day checklist, incident/rollback authority, and post-launch monitoring
|
||||
|
||||
## Launch-day checklist
|
||||
|
||||
Freeze the approved commit and manifest; verify environment and secret inventory; deploy with production integrations disabled until each gate is approved; run localized route, demo, headers, metadata, analytics/consent, privacy, accessibility, performance, and destination smoke checks; confirm monitoring; record go/no-go decision.
|
||||
|
||||
## Post-launch smoke and monitoring checklist
|
||||
|
||||
Verify EN/FR/AR pages, light/dark/system, mobile/desktop, demo acceptance semantics, no duplicate leads, no PII in URLs/logs/analytics, CRM/scheduler latency, errors/timeouts/rate limits, consent withdrawal, security headers, canonical/hreflang/robots, Core Web Vitals, and rollback triggers.
|
||||
|
||||
## Files Phase 16 may modify
|
||||
|
||||
Approved copy/resources, final governed assets, production environment templates without secrets, approved destination/provider adapters, deployment configuration, launch documentation, and targeted tests/evidence.
|
||||
|
||||
## Files Phase 16 must not modify without reopening QA
|
||||
|
||||
Phase 9 contracts, component behavior, page structure, validation/schema, data flow, consent semantics, analytics event names/properties, integration interfaces, accessibility behavior, RTL/theme/routing foundations, CSP/security controls, or performance-critical loading behavior.
|
||||
|
||||
Any change in those areas requires targeted Phase 15 regression and updated evidence before release.
|
||||
|
||||
## Definition of done for release
|
||||
|
||||
A real commit SHA and manifest identify the candidate; all required commands pass; browser/AT/visual/performance/deployment/rollback evidence is approved; no unresolved Critical or High defect remains without accountable written exception; all production business/legal/operational gates are approved; post-deployment smoke checks pass; monitoring and rollback owners are active.
|
||||
'''
|
||||
(ROOT/'docs/PHASE_16_HANDOFF_v1.0.md').write_text(handoff,encoding='utf-8')
|
||||
|
||||
print('Generated Phase 15 reports and matrices.')
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const manifestPath = path.join(root, 'PHASE_16_PACKAGE_MANIFEST_v1.0.json');
|
||||
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']);
|
||||
const excludedFiles = new Set(['PHASE_16_PACKAGE_MANIFEST_v1.0.json', 'package-lock.json', 'tsconfig.tsbuildinfo']);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(root, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push({ fullPath, relative });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = (await walk(root)).sort((a, b) => a.relative.localeCompare(b.relative));
|
||||
const entries = [];
|
||||
for (const file of files) {
|
||||
const contents = await readFile(file.fullPath);
|
||||
const details = await stat(file.fullPath);
|
||||
entries.push({ path: file.relative, bytes: details.size, sha256: createHash('sha256').update(contents).digest('hex') });
|
||||
}
|
||||
const manifest = {
|
||||
schema_version: '1.0',
|
||||
project: 'RentalDriveGo',
|
||||
phase: 16,
|
||||
generated_at: '2026-06-25',
|
||||
release_status: 'Launch aborted before deployment',
|
||||
launch_authorization: 'Rejected',
|
||||
excludes: [...excludedDirectories, ...excludedFiles],
|
||||
file_count: entries.length,
|
||||
files: entries,
|
||||
};
|
||||
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`Generated Phase 16 manifest for ${entries.length} files.`);
|
||||
Executable
+324
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency-free Phase 15 repository integrity, privacy, and evidence audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
EXCLUDED_DIRS = {'.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__'}
|
||||
EXCLUDED_AUDIT_OUTPUTS = {
|
||||
'docs/quality/PHASE_15_STATIC_AUDIT_RESULTS_v1.0.json',
|
||||
'docs/quality/PHASE_15_STATIC_AUDIT_RESULTS_v1.0.md',
|
||||
'docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json',
|
||||
'docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.md',
|
||||
}
|
||||
TEXT_SUFFIXES = {
|
||||
'.css', '.csv', '.html', '.js', '.json', '.jsx', '.md', '.mjs', '.mts', '.txt', '.ts', '.tsx', '.yml', '.yaml'
|
||||
}
|
||||
RESERVED_EMAIL_SUFFIXES = ('.test', '.invalid', '.example')
|
||||
ALLOWED_EMAIL_DOMAINS = {'example.com', 'example.org', 'example.net'}
|
||||
SECRET_PATTERNS = {
|
||||
'private_key': re.compile(r'-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'),
|
||||
'aws_access_key': re.compile(r'\bAKIA[0-9A-Z]{16}\b'),
|
||||
'google_api_key': re.compile(r'\bAIza[0-9A-Za-z_-]{35}\b'),
|
||||
'github_token': re.compile(r'\bgh[pousr]_[0-9A-Za-z]{20,}\b'),
|
||||
'slack_token': re.compile(r'\bxox[baprs]-[0-9A-Za-z-]{10,}\b'),
|
||||
'stripe_secret': re.compile(r'\bsk_(?:live|test)_[0-9A-Za-z]{16,}\b'),
|
||||
}
|
||||
EMAIL_RE = re.compile(r'(?<![\w.+-])([A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))')
|
||||
RUNTIME_EXTERNAL_URL_RE = re.compile(r'https?://[^\s\)\]"\'<>]+')
|
||||
|
||||
|
||||
def walk_files(root: Path):
|
||||
for path in root.rglob('*'):
|
||||
if any(part in EXCLUDED_DIRS for part in path.relative_to(root).parts):
|
||||
continue
|
||||
relative = path.relative_to(root).as_posix()
|
||||
if relative in EXCLUDED_AUDIT_OUTPUTS:
|
||||
continue
|
||||
if path.is_file() or path.is_symlink():
|
||||
yield path
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def read_text(path: Path) -> str | None:
|
||||
if path.suffix.lower() not in TEXT_SUFFIXES and path.name not in {'.env.example', '.gitignore', '.npmrc', '.nvmrc', '.node-version', '.editorconfig', '.prettierignore', '.dockerignore'}:
|
||||
return None
|
||||
try:
|
||||
return path.read_text(encoding='utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def add(findings: list[dict[str, Any]], severity: str, code: str, path: str, message: str, line: int | None = None):
|
||||
item = {'severity': severity, 'code': code, 'path': path, 'message': message}
|
||||
if line is not None:
|
||||
item['line'] = line
|
||||
findings.append(item)
|
||||
|
||||
|
||||
def audit(root: Path) -> dict[str, Any]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
metrics: dict[str, Any] = {}
|
||||
files = list(walk_files(root))
|
||||
regular_files = [p for p in files if p.is_file() and not p.is_symlink()]
|
||||
metrics['file_count'] = len(regular_files)
|
||||
metrics['total_bytes'] = sum(p.stat().st_size for p in regular_files)
|
||||
metrics['suffix_counts'] = dict(sorted(Counter(p.suffix.lower() or '<none>' for p in regular_files).items()))
|
||||
|
||||
# Filesystem integrity.
|
||||
for p in files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if p.is_symlink():
|
||||
add(findings, 'High', 'symlink_present', rel, f'Symlink target: {os.readlink(p)}')
|
||||
elif p.stat().st_mode & stat.S_IWOTH:
|
||||
add(findings, 'Medium', 'world_writable', rel, 'Repository file is world-writable.')
|
||||
for directory in EXCLUDED_DIRS - {'.git'}:
|
||||
if (root / directory).exists():
|
||||
add(findings, 'Medium', 'generated_directory_present', directory, 'Generated/cache directory is present in the release package.')
|
||||
for forbidden in ('.env', '.env.local', '.env.production', 'package-lock.json', 'tsconfig.tsbuildinfo'):
|
||||
if (root / forbidden).exists():
|
||||
add(findings, 'High' if forbidden.startswith('.env') else 'Medium', 'forbidden_root_file', forbidden, 'Unexpected generated or secret-bearing root file.')
|
||||
|
||||
# JSON and CSV structural validity.
|
||||
json_count = 0
|
||||
csv_count = 0
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if p.suffix.lower() == '.json':
|
||||
json_count += 1
|
||||
try:
|
||||
json.loads(p.read_text(encoding='utf-8'))
|
||||
except Exception as exc:
|
||||
add(findings, 'High', 'invalid_json', rel, str(exc))
|
||||
elif p.suffix.lower() == '.csv':
|
||||
csv_count += 1
|
||||
try:
|
||||
with p.open(newline='', encoding='utf-8-sig') as f:
|
||||
rows = list(csv.reader(f))
|
||||
if rows:
|
||||
width = len(rows[0])
|
||||
for index, row in enumerate(rows, start=1):
|
||||
if len(row) != width:
|
||||
add(findings, 'High', 'malformed_csv', rel, f'Expected {width} columns, found {len(row)}.', index)
|
||||
except Exception as exc:
|
||||
add(findings, 'High', 'invalid_csv', rel, str(exc))
|
||||
metrics['json_files_parsed'] = json_count
|
||||
metrics['csv_files_parsed'] = csv_count
|
||||
|
||||
# Secret, PII-fixture, and unsafe runtime scans.
|
||||
secret_scan_files = 0
|
||||
email_hits: list[dict[str, Any]] = []
|
||||
runtime_urls: list[dict[str, Any]] = []
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
text = read_text(p)
|
||||
if text is None:
|
||||
continue
|
||||
lines = text.splitlines()
|
||||
if rel not in {'scripts/phase15-static-audit.py'} and not rel.startswith('PHASE_') and p.name != 'pnpm-lock.yaml':
|
||||
secret_scan_files += 1
|
||||
for name, pattern in SECRET_PATTERNS.items():
|
||||
for match in pattern.finditer(text):
|
||||
line = text.count('\n', 0, match.start()) + 1
|
||||
add(findings, 'Critical', f'secret_{name}', rel, 'Potential live credential or private key pattern.', line)
|
||||
if not rel.startswith('contracts/phase9/') and p.name != 'pnpm-lock.yaml':
|
||||
for index, line_text in enumerate(lines, start=1):
|
||||
for match in EMAIL_RE.finditer(line_text):
|
||||
address, domain = match.group(1), match.group(2).lower()
|
||||
email_hits.append({'path': rel, 'line': index, 'address': address})
|
||||
if not domain.endswith(RESERVED_EMAIL_SUFFIXES) and domain not in ALLOWED_EMAIL_DOMAINS:
|
||||
add(findings, 'High', 'non_reserved_email_fixture', rel, f'Non-reserved email address found: {address}', index)
|
||||
if rel.startswith('src/') or rel in {'.env.example', 'next.config.ts'}:
|
||||
for index, line_text in enumerate(lines, start=1):
|
||||
for match in RUNTIME_EXTERNAL_URL_RE.finditer(line_text):
|
||||
url = match.group(0).rstrip('.,;')
|
||||
runtime_urls.append({'path': rel, 'line': index, 'url': url})
|
||||
allowed = (
|
||||
url.startswith('http://localhost')
|
||||
or url.startswith('http://127.0.0.1')
|
||||
or url.startswith('https://your-approved-domain.example')
|
||||
or url.startswith('https://design-tokens.github.io/')
|
||||
)
|
||||
if not allowed:
|
||||
add(findings, 'High', 'unapproved_runtime_url', rel, f'Runtime/config URL is not on the documented safe list: {url}', index)
|
||||
metrics['secret_scan_files'] = secret_scan_files
|
||||
metrics['email_fixture_hits'] = email_hits
|
||||
metrics['runtime_url_hits'] = runtime_urls
|
||||
|
||||
# Runtime anti-patterns and release-control invariants.
|
||||
runtime_text = ''
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if rel.startswith('src/'):
|
||||
text = read_text(p)
|
||||
if text is not None:
|
||||
runtime_text += f'\n/* {rel} */\n{text}'
|
||||
for index, line_text in enumerate(text.splitlines(), start=1):
|
||||
if re.search(r'\b(TODO|TBD|FIXME|lorem ipsum)\b', line_text, re.I):
|
||||
add(findings, 'Medium', 'runtime_placeholder', rel, line_text.strip(), index)
|
||||
if re.search(r'console\.(?:log|info|warn|error|debug)\s*\(', line_text):
|
||||
add(findings, 'High', 'runtime_console_logging', rel, 'Runtime console logging requires privacy review.', index)
|
||||
if re.search(r'\beval\s*\(|new\s+Function\s*\(', line_text):
|
||||
add(findings, 'Critical', 'dynamic_code_execution', rel, 'Dynamic code execution found.', index)
|
||||
if "startsWith('application/json')" in runtime_text or 'startsWith("application/json")' in runtime_text:
|
||||
add(findings, 'Medium', 'loose_content_type_match', 'src/', 'JSON API still uses a prefix media-type check.')
|
||||
helper = root / 'src/lib/security/content-type.ts'
|
||||
if not helper.exists() or "=== 'application/json'" not in helper.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'missing_exact_content_type_control', helper.relative_to(root).as_posix(), 'Exact application/json media-type helper is absent.')
|
||||
route = root / 'src/app/api/demo/route.ts'
|
||||
if route.exists() and 'isApplicationJson(contentType)' not in route.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'content_type_helper_not_used', route.relative_to(root).as_posix(), 'Demo API does not use exact JSON media-type validation.')
|
||||
|
||||
# Visual-test determinism invariants.
|
||||
visual_component = root / 'tests/visual/component-system.visual.spec.ts'
|
||||
if visual_component.exists():
|
||||
text = visual_component.read_text(encoding='utf-8')
|
||||
if 'rdg-theme-preference' in text:
|
||||
add(findings, 'Medium', 'stale_theme_storage_key', visual_component.relative_to(root).as_posix(), 'Visual test writes a storage key the application never reads.')
|
||||
if "hpc.theme.preference" not in text:
|
||||
add(findings, 'Medium', 'theme_storage_key_missing', visual_component.relative_to(root).as_posix(), 'Visual test does not use the canonical theme storage key.')
|
||||
if "project.name !== 'chromium'" not in text:
|
||||
add(findings, 'Medium', 'visual_project_scope', visual_component.relative_to(root).as_posix(), 'Canonical snapshot test is not restricted to Chromium.')
|
||||
visual_integrations = root / 'tests/visual/integrations.visual.spec.ts'
|
||||
if visual_integrations.exists() and "project.name !== 'chromium'" not in visual_integrations.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'visual_project_scope', visual_integrations.relative_to(root).as_posix(), 'Canonical integration snapshots are not restricted to Chromium.')
|
||||
|
||||
# CI must execute the integration suite; unit and browser suites do not cover the server adapter orchestration.
|
||||
workflow_path = root / '.github/workflows/ci.yml'
|
||||
if workflow_path.exists():
|
||||
workflow_text = workflow_path.read_text(encoding='utf-8')
|
||||
if 'pnpm test:integration' not in workflow_text:
|
||||
add(findings, 'High', 'ci_integration_suite_omitted', workflow_path.relative_to(root).as_posix(), 'CI does not execute the demo-service integration suite.')
|
||||
|
||||
# Lockfile/direct dependency checks are delegated to the repository validator; record pinning shape here.
|
||||
package = json.loads((root / 'package.json').read_text(encoding='utf-8'))
|
||||
direct = {**package.get('dependencies', {}), **package.get('devDependencies', {})}
|
||||
floating = {name: value for name, value in direct.items() if re.search(r'[~^*xX<>| ]', str(value))}
|
||||
if floating:
|
||||
add(findings, 'High', 'floating_direct_dependencies', 'package.json', json.dumps(floating, sort_keys=True))
|
||||
metrics['direct_dependency_count'] = len(direct)
|
||||
metrics['direct_dependencies_exact'] = not floating
|
||||
|
||||
# Issue/gate metrics.
|
||||
def issue_version(path: Path) -> tuple[int, ...]:
|
||||
match = re.search(r'_v(\d+(?:\.\d+)*)\.csv$', path.name)
|
||||
return tuple(int(part) for part in match.group(1).split('.')) if match else (0,)
|
||||
|
||||
issue_candidates = sorted(
|
||||
(root / 'docs').glob('IMPLEMENTATION_ISSUE_REGISTER_v*.csv'),
|
||||
key=issue_version,
|
||||
)
|
||||
issue_path = issue_candidates[-1] if issue_candidates else None
|
||||
if issue_path is not None:
|
||||
with issue_path.open(newline='', encoding='utf-8-sig') as f:
|
||||
issues = list(csv.DictReader(f))
|
||||
metrics['issue_register_path'] = issue_path.relative_to(root).as_posix()
|
||||
metrics['issue_register_rows'] = len(issues)
|
||||
unresolved = [r for r in issues if 'resolved' not in (r.get('Status') or '').lower()]
|
||||
metrics['unresolved_issue_counts'] = dict(Counter(r.get('Severity', 'Unknown') for r in unresolved))
|
||||
if any(None in r for r in issues):
|
||||
add(findings, 'High', 'issue_register_overflow', issue_path.relative_to(root).as_posix(), 'One or more issue-register rows contain overflow columns.')
|
||||
gate_candidates = [
|
||||
root / 'docs/phase15/RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
root / 'docs/phase14/RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
]
|
||||
gate_path = next((candidate for candidate in gate_candidates if candidate.exists()), None)
|
||||
if gate_path is not None:
|
||||
with gate_path.open(newline='', encoding='utf-8-sig') as f:
|
||||
gates = list(csv.DictReader(f))
|
||||
metrics['release_gate_path'] = gate_path.relative_to(root).as_posix()
|
||||
metrics['release_gate_status_counts'] = dict(Counter(r.get('Status', 'Unknown') for r in gates))
|
||||
|
||||
# High-level privacy invariants.
|
||||
api_text = route.read_text(encoding='utf-8') if route.exists() else ''
|
||||
required_api_controls = {
|
||||
'request_marker': "x-rdg-form",
|
||||
'origin_validation': 'isAllowedOrigin',
|
||||
'body_limit': 'maximumBodyBytes',
|
||||
'no_store': "'Cache-Control': 'no-store'",
|
||||
'honeypot': 'honeypot',
|
||||
}
|
||||
for code, token in required_api_controls.items():
|
||||
if token not in api_text:
|
||||
add(findings, 'High', f'missing_api_{code}', route.relative_to(root).as_posix(), f'Missing expected API control token: {token}')
|
||||
analytics_text = (root / 'src/lib/analytics/events.ts').read_text(encoding='utf-8')
|
||||
for forbidden in ('email', 'company', 'message', 'phone', 'fieldvalue', 'serverresponse'):
|
||||
if f"'{forbidden}'" not in analytics_text:
|
||||
add(findings, 'High', 'analytics_forbidden_key_missing', 'src/lib/analytics/events.ts', f'Forbidden analytics key is not explicitly denied: {forbidden}')
|
||||
|
||||
severity_rank = {'Critical': 4, 'High': 3, 'Medium': 2, 'Low': 1, 'Info': 0}
|
||||
findings.sort(key=lambda x: (-severity_rank.get(x['severity'], 0), x['path'], x.get('line', 0), x['code']))
|
||||
counts = Counter(item['severity'] for item in findings)
|
||||
return {
|
||||
'schema_version': '1.0',
|
||||
'audit': 'Phase 15 dependency-free static repository audit',
|
||||
'root': str(root),
|
||||
'metrics': metrics,
|
||||
'finding_counts': dict(counts),
|
||||
'findings': findings,
|
||||
'passed': not any(item['severity'] in {'Critical', 'High', 'Medium'} for item in findings),
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(result: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
'# Phase 15 Static Repository Audit',
|
||||
'',
|
||||
f"- Result: **{'PASS' if result['passed'] else 'FAIL'}**",
|
||||
f"- Files inspected: {result['metrics']['file_count']}",
|
||||
f"- JSON files parsed: {result['metrics']['json_files_parsed']}",
|
||||
f"- CSV files parsed: {result['metrics']['csv_files_parsed']}",
|
||||
f"- Secret-scan files: {result['metrics']['secret_scan_files']}",
|
||||
f"- Findings: {json.dumps(result['finding_counts'], sort_keys=True)}",
|
||||
'',
|
||||
'## Findings',
|
||||
'',
|
||||
]
|
||||
if not result['findings']:
|
||||
lines.append('No Critical, High, Medium, Low, or informational findings were produced by this audit.')
|
||||
else:
|
||||
lines.append('| Severity | Code | Path | Line | Finding |')
|
||||
lines.append('|---|---|---|---:|---|')
|
||||
for item in result['findings']:
|
||||
message = str(item['message']).replace('|', '\\|').replace('\n', ' ')
|
||||
lines.append(f"| {item['severity']} | `{item['code']}` | `{item['path']}` | {item.get('line', '')} | {message} |")
|
||||
lines.extend(['', '## Metrics', '', '```json', json.dumps(result['metrics'], indent=2, sort_keys=True), '```', ''])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument('--json-output', type=Path)
|
||||
parser.add_argument('--markdown-output', type=Path)
|
||||
args = parser.parse_args()
|
||||
result = audit(args.root.resolve())
|
||||
if args.json_output:
|
||||
args.json_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json_output.write_text(json.dumps(result, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||
if args.markdown_output:
|
||||
args.markdown_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.markdown_output.write_text(render_markdown(result), encoding='utf-8')
|
||||
print(json.dumps({'passed': result['passed'], 'finding_counts': result['finding_counts'], 'files': result['metrics']['file_count']}, sort_keys=True))
|
||||
return 0 if result['passed'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Executable
+325
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency-free Phase 16 repository integrity, privacy, and release-governance audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
EXCLUDED_DIRS = {'.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__'}
|
||||
EXCLUDED_AUDIT_OUTPUTS = {
|
||||
'docs/quality/PHASE_15_STATIC_AUDIT_RESULTS_v1.0.json',
|
||||
'docs/quality/PHASE_15_STATIC_AUDIT_RESULTS_v1.0.md',
|
||||
'docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.json',
|
||||
'docs/quality/PHASE_15_BASELINE_STATIC_AUDIT_RESULTS_v1.0.md',
|
||||
}
|
||||
TEXT_SUFFIXES = {
|
||||
'.css', '.csv', '.html', '.js', '.json', '.jsx', '.md', '.mjs', '.mts', '.txt', '.ts', '.tsx', '.yml', '.yaml'
|
||||
}
|
||||
RESERVED_EMAIL_SUFFIXES = ('.test', '.invalid', '.example')
|
||||
ALLOWED_EMAIL_DOMAINS = {'example.com', 'example.org', 'example.net'}
|
||||
SECRET_PATTERNS = {
|
||||
'private_key': re.compile(r'-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'),
|
||||
'aws_access_key': re.compile(r'\bAKIA[0-9A-Z]{16}\b'),
|
||||
'google_api_key': re.compile(r'\bAIza[0-9A-Za-z_-]{35}\b'),
|
||||
'github_token': re.compile(r'\bgh[pousr]_[0-9A-Za-z]{20,}\b'),
|
||||
'slack_token': re.compile(r'\bxox[baprs]-[0-9A-Za-z-]{10,}\b'),
|
||||
'stripe_secret': re.compile(r'\bsk_(?:live|test)_[0-9A-Za-z]{16,}\b'),
|
||||
}
|
||||
EMAIL_RE = re.compile(r'(?<![\w.+-])([A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))')
|
||||
RUNTIME_EXTERNAL_URL_RE = re.compile(r'https?://[^\s\)\]"\'<>]+')
|
||||
|
||||
|
||||
def walk_files(root: Path):
|
||||
for path in root.rglob('*'):
|
||||
if any(part in EXCLUDED_DIRS for part in path.relative_to(root).parts):
|
||||
continue
|
||||
relative = path.relative_to(root).as_posix()
|
||||
if relative in EXCLUDED_AUDIT_OUTPUTS:
|
||||
continue
|
||||
if path.is_file() or path.is_symlink():
|
||||
yield path
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def read_text(path: Path) -> str | None:
|
||||
if path.suffix.lower() not in TEXT_SUFFIXES and path.name not in {'.env.example', '.gitignore', '.npmrc', '.nvmrc', '.node-version', '.editorconfig', '.prettierignore', '.dockerignore'}:
|
||||
return None
|
||||
try:
|
||||
return path.read_text(encoding='utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def add(findings: list[dict[str, Any]], severity: str, code: str, path: str, message: str, line: int | None = None):
|
||||
item = {'severity': severity, 'code': code, 'path': path, 'message': message}
|
||||
if line is not None:
|
||||
item['line'] = line
|
||||
findings.append(item)
|
||||
|
||||
|
||||
def audit(root: Path) -> dict[str, Any]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
metrics: dict[str, Any] = {}
|
||||
files = list(walk_files(root))
|
||||
regular_files = [p for p in files if p.is_file() and not p.is_symlink()]
|
||||
metrics['file_count'] = len(regular_files)
|
||||
metrics['total_bytes'] = sum(p.stat().st_size for p in regular_files)
|
||||
metrics['suffix_counts'] = dict(sorted(Counter(p.suffix.lower() or '<none>' for p in regular_files).items()))
|
||||
|
||||
# Filesystem integrity.
|
||||
for p in files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if p.is_symlink():
|
||||
add(findings, 'High', 'symlink_present', rel, f'Symlink target: {os.readlink(p)}')
|
||||
elif p.stat().st_mode & stat.S_IWOTH:
|
||||
add(findings, 'Medium', 'world_writable', rel, 'Repository file is world-writable.')
|
||||
for directory in EXCLUDED_DIRS - {'.git'}:
|
||||
if (root / directory).exists():
|
||||
add(findings, 'Medium', 'generated_directory_present', directory, 'Generated/cache directory is present in the release package.')
|
||||
for forbidden in ('.env', '.env.local', '.env.production', 'package-lock.json', 'tsconfig.tsbuildinfo'):
|
||||
if (root / forbidden).exists():
|
||||
add(findings, 'High' if forbidden.startswith('.env') else 'Medium', 'forbidden_root_file', forbidden, 'Unexpected generated or secret-bearing root file.')
|
||||
|
||||
# JSON and CSV structural validity.
|
||||
json_count = 0
|
||||
csv_count = 0
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if p.suffix.lower() == '.json':
|
||||
json_count += 1
|
||||
try:
|
||||
json.loads(p.read_text(encoding='utf-8'))
|
||||
except Exception as exc:
|
||||
add(findings, 'High', 'invalid_json', rel, str(exc))
|
||||
elif p.suffix.lower() == '.csv':
|
||||
csv_count += 1
|
||||
try:
|
||||
with p.open(newline='', encoding='utf-8-sig') as f:
|
||||
rows = list(csv.reader(f))
|
||||
if rows:
|
||||
width = len(rows[0])
|
||||
for index, row in enumerate(rows, start=1):
|
||||
if len(row) != width:
|
||||
add(findings, 'High', 'malformed_csv', rel, f'Expected {width} columns, found {len(row)}.', index)
|
||||
except Exception as exc:
|
||||
add(findings, 'High', 'invalid_csv', rel, str(exc))
|
||||
metrics['json_files_parsed'] = json_count
|
||||
metrics['csv_files_parsed'] = csv_count
|
||||
|
||||
# Secret, PII-fixture, and unsafe runtime scans.
|
||||
secret_scan_files = 0
|
||||
email_hits: list[dict[str, Any]] = []
|
||||
runtime_urls: list[dict[str, Any]] = []
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
text = read_text(p)
|
||||
if text is None:
|
||||
continue
|
||||
lines = text.splitlines()
|
||||
if rel not in {'scripts/phase15-static-audit.py', 'scripts/phase16-static-audit.py'} and not rel.startswith('PHASE_') and p.name != 'pnpm-lock.yaml':
|
||||
secret_scan_files += 1
|
||||
for name, pattern in SECRET_PATTERNS.items():
|
||||
for match in pattern.finditer(text):
|
||||
line = text.count('\n', 0, match.start()) + 1
|
||||
add(findings, 'Critical', f'secret_{name}', rel, 'Potential live credential or private key pattern.', line)
|
||||
if not rel.startswith('contracts/phase9/') and p.name != 'pnpm-lock.yaml':
|
||||
for index, line_text in enumerate(lines, start=1):
|
||||
for match in EMAIL_RE.finditer(line_text):
|
||||
address, domain = match.group(1), match.group(2).lower()
|
||||
email_hits.append({'path': rel, 'line': index, 'address': address})
|
||||
if not domain.endswith(RESERVED_EMAIL_SUFFIXES) and domain not in ALLOWED_EMAIL_DOMAINS:
|
||||
add(findings, 'High', 'non_reserved_email_fixture', rel, f'Non-reserved email address found: {address}', index)
|
||||
if rel.startswith('src/') or rel in {'.env.example', 'next.config.ts'}:
|
||||
for index, line_text in enumerate(lines, start=1):
|
||||
for match in RUNTIME_EXTERNAL_URL_RE.finditer(line_text):
|
||||
url = match.group(0).rstrip('.,;')
|
||||
runtime_urls.append({'path': rel, 'line': index, 'url': url})
|
||||
allowed = (
|
||||
url.startswith('http://localhost')
|
||||
or url.startswith('http://127.0.0.1')
|
||||
or url.startswith('https://your-approved-domain.example')
|
||||
or url.startswith('https://design-tokens.github.io/')
|
||||
)
|
||||
if not allowed:
|
||||
add(findings, 'High', 'unapproved_runtime_url', rel, f'Runtime/config URL is not on the documented safe list: {url}', index)
|
||||
metrics['secret_scan_files'] = secret_scan_files
|
||||
metrics['email_fixture_hits'] = email_hits
|
||||
metrics['runtime_url_hits'] = runtime_urls
|
||||
|
||||
# Runtime anti-patterns and release-control invariants.
|
||||
runtime_text = ''
|
||||
for p in regular_files:
|
||||
rel = p.relative_to(root).as_posix()
|
||||
if rel.startswith('src/'):
|
||||
text = read_text(p)
|
||||
if text is not None:
|
||||
runtime_text += f'\n/* {rel} */\n{text}'
|
||||
for index, line_text in enumerate(text.splitlines(), start=1):
|
||||
if re.search(r'\b(TODO|TBD|FIXME|lorem ipsum)\b', line_text, re.I):
|
||||
add(findings, 'Medium', 'runtime_placeholder', rel, line_text.strip(), index)
|
||||
if re.search(r'console\.(?:log|info|warn|error|debug)\s*\(', line_text):
|
||||
add(findings, 'High', 'runtime_console_logging', rel, 'Runtime console logging requires privacy review.', index)
|
||||
if re.search(r'\beval\s*\(|new\s+Function\s*\(', line_text):
|
||||
add(findings, 'Critical', 'dynamic_code_execution', rel, 'Dynamic code execution found.', index)
|
||||
if "startsWith('application/json')" in runtime_text or 'startsWith("application/json")' in runtime_text:
|
||||
add(findings, 'Medium', 'loose_content_type_match', 'src/', 'JSON API still uses a prefix media-type check.')
|
||||
helper = root / 'src/lib/security/content-type.ts'
|
||||
if not helper.exists() or "=== 'application/json'" not in helper.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'missing_exact_content_type_control', helper.relative_to(root).as_posix(), 'Exact application/json media-type helper is absent.')
|
||||
route = root / 'src/app/api/demo/route.ts'
|
||||
if route.exists() and 'isApplicationJson(contentType)' not in route.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'content_type_helper_not_used', route.relative_to(root).as_posix(), 'Demo API does not use exact JSON media-type validation.')
|
||||
|
||||
# Visual-test determinism invariants.
|
||||
visual_component = root / 'tests/visual/component-system.visual.spec.ts'
|
||||
if visual_component.exists():
|
||||
text = visual_component.read_text(encoding='utf-8')
|
||||
if 'rdg-theme-preference' in text:
|
||||
add(findings, 'Medium', 'stale_theme_storage_key', visual_component.relative_to(root).as_posix(), 'Visual test writes a storage key the application never reads.')
|
||||
if "hpc.theme.preference" not in text:
|
||||
add(findings, 'Medium', 'theme_storage_key_missing', visual_component.relative_to(root).as_posix(), 'Visual test does not use the canonical theme storage key.')
|
||||
if "project.name !== 'chromium'" not in text:
|
||||
add(findings, 'Medium', 'visual_project_scope', visual_component.relative_to(root).as_posix(), 'Canonical snapshot test is not restricted to Chromium.')
|
||||
visual_integrations = root / 'tests/visual/integrations.visual.spec.ts'
|
||||
if visual_integrations.exists() and "project.name !== 'chromium'" not in visual_integrations.read_text(encoding='utf-8'):
|
||||
add(findings, 'Medium', 'visual_project_scope', visual_integrations.relative_to(root).as_posix(), 'Canonical integration snapshots are not restricted to Chromium.')
|
||||
|
||||
# CI must execute the integration suite; unit and browser suites do not cover the server adapter orchestration.
|
||||
workflow_path = root / '.github/workflows/ci.yml'
|
||||
if workflow_path.exists():
|
||||
workflow_text = workflow_path.read_text(encoding='utf-8')
|
||||
if 'pnpm test:integration' not in workflow_text:
|
||||
add(findings, 'High', 'ci_integration_suite_omitted', workflow_path.relative_to(root).as_posix(), 'CI does not execute the demo-service integration suite.')
|
||||
|
||||
# Lockfile/direct dependency checks are delegated to the repository validator; record pinning shape here.
|
||||
package = json.loads((root / 'package.json').read_text(encoding='utf-8'))
|
||||
direct = {**package.get('dependencies', {}), **package.get('devDependencies', {})}
|
||||
floating = {name: value for name, value in direct.items() if re.search(r'[~^*xX<>| ]', str(value))}
|
||||
if floating:
|
||||
add(findings, 'High', 'floating_direct_dependencies', 'package.json', json.dumps(floating, sort_keys=True))
|
||||
metrics['direct_dependency_count'] = len(direct)
|
||||
metrics['direct_dependencies_exact'] = not floating
|
||||
|
||||
# Issue/gate metrics.
|
||||
def issue_version(path: Path) -> tuple[int, ...]:
|
||||
match = re.search(r'_v(\d+(?:\.\d+)*)\.csv$', path.name)
|
||||
return tuple(int(part) for part in match.group(1).split('.')) if match else (0,)
|
||||
|
||||
issue_candidates = sorted(
|
||||
(root / 'docs').glob('IMPLEMENTATION_ISSUE_REGISTER_v*.csv'),
|
||||
key=issue_version,
|
||||
)
|
||||
issue_path = issue_candidates[-1] if issue_candidates else None
|
||||
if issue_path is not None:
|
||||
with issue_path.open(newline='', encoding='utf-8-sig') as f:
|
||||
issues = list(csv.DictReader(f))
|
||||
metrics['issue_register_path'] = issue_path.relative_to(root).as_posix()
|
||||
metrics['issue_register_rows'] = len(issues)
|
||||
unresolved = [r for r in issues if 'resolved' not in (r.get('Status') or '').lower()]
|
||||
metrics['unresolved_issue_counts'] = dict(Counter(r.get('Severity', 'Unknown') for r in unresolved))
|
||||
if any(None in r for r in issues):
|
||||
add(findings, 'High', 'issue_register_overflow', issue_path.relative_to(root).as_posix(), 'One or more issue-register rows contain overflow columns.')
|
||||
gate_candidates = [
|
||||
root / 'docs/phase16/FINAL_RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
root / 'docs/phase15/RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
root / 'docs/phase14/RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
]
|
||||
gate_path = next((candidate for candidate in gate_candidates if candidate.exists()), None)
|
||||
if gate_path is not None:
|
||||
with gate_path.open(newline='', encoding='utf-8-sig') as f:
|
||||
gates = list(csv.DictReader(f))
|
||||
metrics['release_gate_path'] = gate_path.relative_to(root).as_posix()
|
||||
metrics['release_gate_status_counts'] = dict(Counter(r.get('Phase16_decision') or r.get('Status', 'Unknown') for r in gates))
|
||||
|
||||
# High-level privacy invariants.
|
||||
api_text = route.read_text(encoding='utf-8') if route.exists() else ''
|
||||
required_api_controls = {
|
||||
'request_marker': "x-rdg-form",
|
||||
'origin_validation': 'isAllowedOrigin',
|
||||
'body_limit': 'maximumBodyBytes',
|
||||
'no_store': "'Cache-Control': 'no-store'",
|
||||
'honeypot': 'honeypot',
|
||||
}
|
||||
for code, token in required_api_controls.items():
|
||||
if token not in api_text:
|
||||
add(findings, 'High', f'missing_api_{code}', route.relative_to(root).as_posix(), f'Missing expected API control token: {token}')
|
||||
analytics_text = (root / 'src/lib/analytics/events.ts').read_text(encoding='utf-8')
|
||||
for forbidden in ('email', 'company', 'message', 'phone', 'fieldvalue', 'serverresponse'):
|
||||
if f"'{forbidden}'" not in analytics_text:
|
||||
add(findings, 'High', 'analytics_forbidden_key_missing', 'src/lib/analytics/events.ts', f'Forbidden analytics key is not explicitly denied: {forbidden}')
|
||||
|
||||
severity_rank = {'Critical': 4, 'High': 3, 'Medium': 2, 'Low': 1, 'Info': 0}
|
||||
findings.sort(key=lambda x: (-severity_rank.get(x['severity'], 0), x['path'], x.get('line', 0), x['code']))
|
||||
counts = Counter(item['severity'] for item in findings)
|
||||
return {
|
||||
'schema_version': '1.0',
|
||||
'audit': 'Phase 16 dependency-free static repository audit',
|
||||
'root': str(root),
|
||||
'metrics': metrics,
|
||||
'finding_counts': dict(counts),
|
||||
'findings': findings,
|
||||
'passed': not any(item['severity'] in {'Critical', 'High', 'Medium'} for item in findings),
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(result: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
'# Phase 16 Static Repository Audit',
|
||||
'',
|
||||
f"- Result: **{'PASS' if result['passed'] else 'FAIL'}**",
|
||||
f"- Files inspected: {result['metrics']['file_count']}",
|
||||
f"- JSON files parsed: {result['metrics']['json_files_parsed']}",
|
||||
f"- CSV files parsed: {result['metrics']['csv_files_parsed']}",
|
||||
f"- Secret-scan files: {result['metrics']['secret_scan_files']}",
|
||||
f"- Findings: {json.dumps(result['finding_counts'], sort_keys=True)}",
|
||||
'',
|
||||
'## Findings',
|
||||
'',
|
||||
]
|
||||
if not result['findings']:
|
||||
lines.append('No Critical, High, Medium, Low, or informational findings were produced by this audit.')
|
||||
else:
|
||||
lines.append('| Severity | Code | Path | Line | Finding |')
|
||||
lines.append('|---|---|---|---:|---|')
|
||||
for item in result['findings']:
|
||||
message = str(item['message']).replace('|', '\\|').replace('\n', ' ')
|
||||
lines.append(f"| {item['severity']} | `{item['code']}` | `{item['path']}` | {item.get('line', '')} | {message} |")
|
||||
lines.extend(['', '## Metrics', '', '```json', json.dumps(result['metrics'], indent=2, sort_keys=True), '```', ''])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument('--json-output', type=Path)
|
||||
parser.add_argument('--markdown-output', type=Path)
|
||||
args = parser.parse_args()
|
||||
result = audit(args.root.resolve())
|
||||
if args.json_output:
|
||||
args.json_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json_output.write_text(json.dumps(result, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||
if args.markdown_output:
|
||||
args.markdown_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.markdown_output.write_text(render_markdown(result), encoding='utf-8')
|
||||
print(json.dumps({'passed': result['passed'], 'finding_counts': result['finding_counts'], 'files': result['metrics']['file_count']}, sort_keys=True))
|
||||
return 0 if result['passed'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,4 @@
|
||||
import { rm } from 'node:fs/promises';
|
||||
|
||||
await rm('.next/dev/types', { recursive: true, force: true });
|
||||
console.log('Removed conflicting Next.js development route declarations before type checking.');
|
||||
@@ -6,6 +6,9 @@ const commands = [
|
||||
['pnpm', ['lint']],
|
||||
['pnpm', ['typecheck']],
|
||||
['pnpm', ['test:unit']],
|
||||
['pnpm', ['test:integration']],
|
||||
['pnpm', ['test:security']],
|
||||
['pnpm', ['test:privacy']],
|
||||
['pnpm', ['build'], { SITE_ORIGIN: 'http://127.0.0.1:3000', PUBLIC_RELEASE_APPROVED: 'false' }],
|
||||
['pnpm', ['audit']],
|
||||
];
|
||||
|
||||
@@ -12,6 +12,12 @@ const scripts = [
|
||||
'validate-metadata.mjs',
|
||||
'validate-phase12-components.mjs',
|
||||
'validate-phase13-homepage.mjs',
|
||||
'validate-phase14-integrations.mjs',
|
||||
'validate-phase14-environment.mjs',
|
||||
'validate-phase14-security.mjs',
|
||||
'validate-phase14-privacy.mjs',
|
||||
'validate-phase15-readiness.mjs',
|
||||
'validate-phase16-readiness.mjs',
|
||||
];
|
||||
for (const script of scripts) {
|
||||
const result = spawnSync(process.execPath, [`scripts/${script}`], { stdio: 'inherit' });
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
const failures = [];
|
||||
const example = await readFile(path.join(repositoryRoot, '.env.example'), 'utf8');
|
||||
const implementation = await readFile(
|
||||
path.join(repositoryRoot, 'src/lib/integrations/environment.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const keys = [
|
||||
'NEXT_PUBLIC_DEMO_ENABLED',
|
||||
'DEMO_SUBMISSION_MODE',
|
||||
'DEMO_REQUEST_TIMEOUT_MS',
|
||||
'DEMO_IDEMPOTENCY_TTL_SECONDS',
|
||||
'DEMO_TEST_SCENARIOS_ENABLED',
|
||||
'DEMO_LOCAL_TEST_MODE',
|
||||
'NEXT_PUBLIC_ANALYTICS_MODE',
|
||||
];
|
||||
for (const key of keys) {
|
||||
if (!example.includes(`${key}=`)) failures.push(`.env.example omits ${key}.`);
|
||||
if (!implementation.includes(key)) failures.push(`Environment validator omits ${key}.`);
|
||||
}
|
||||
if (!example.includes('DEMO_SUBMISSION_MODE=blocked')) {
|
||||
failures.push('Safe production example does not default demo submission to blocked.');
|
||||
}
|
||||
if (!example.includes('NEXT_PUBLIC_ANALYTICS_MODE=disabled')) {
|
||||
failures.push('Safe production example does not default analytics to disabled.');
|
||||
}
|
||||
|
||||
if (failures.length) fail(failures);
|
||||
else
|
||||
pass(
|
||||
'Phase 14 environment inventory is explicit and defaults production integrations to blocked.',
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
const failures = [];
|
||||
const requiredFiles = [
|
||||
'src/app/api/demo/route.ts',
|
||||
'src/components/integrations/DemoDialogHost.tsx',
|
||||
'src/lib/demo/schema.ts',
|
||||
'src/lib/demo/service.ts',
|
||||
'src/lib/demo/idempotency.ts',
|
||||
'src/lib/demo/adapters/types.ts',
|
||||
'src/lib/demo/adapters/local.ts',
|
||||
'src/lib/demo/adapters/blocked.ts',
|
||||
'src/lib/integrations/destinations.ts',
|
||||
'src/lib/integrations/environment.ts',
|
||||
'src/lib/integrations/feature-flags.ts',
|
||||
'src/lib/analytics/events.ts',
|
||||
'src/lib/analytics/client.ts',
|
||||
'docs/phase14/INTEGRATION_TRACEABILITY_MATRIX_v1.0.csv',
|
||||
'docs/phase14/DATA_FLOW_INVENTORY_v1.0.csv',
|
||||
'docs/phase14/RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
];
|
||||
for (const file of requiredFiles) {
|
||||
try {
|
||||
const details = await stat(path.join(repositoryRoot, file));
|
||||
if (!details.isFile()) failures.push(`${file}: not a file.`);
|
||||
} catch {
|
||||
failures.push(`${file}: missing.`);
|
||||
}
|
||||
}
|
||||
|
||||
const schema = await readFile(path.join(repositoryRoot, 'src/lib/demo/schema.ts'), 'utf8');
|
||||
for (const field of ['fullName', 'workEmail', 'company', 'fleetSize', 'idempotencyKey', 'source']) {
|
||||
if (!schema.includes(`${field}:`)) failures.push(`Demo schema is missing ${field}.`);
|
||||
}
|
||||
if (!schema.includes('.strict()'))
|
||||
failures.push('Demo transport schemas must reject unknown fields.');
|
||||
if (schema.includes('phone:'))
|
||||
failures.push('Unapproved phone field was added to the demo contract.');
|
||||
|
||||
const destinations = await readFile(
|
||||
path.join(repositoryRoot, 'src/lib/integrations/destinations.ts'),
|
||||
'utf8',
|
||||
);
|
||||
for (const issue of ['P9-002', 'P9-003', 'P9-004', 'P9-008', 'P9-012', 'P9-014', 'P9-015']) {
|
||||
if (!destinations.includes(issue))
|
||||
failures.push(`Destination registry does not preserve ${issue}.`);
|
||||
}
|
||||
if (/href:\s*['"]https?:\/\//.test(destinations)) {
|
||||
failures.push('Destination registry contains an unapproved hard-coded external URL.');
|
||||
}
|
||||
|
||||
for (const locale of ['en', 'fr', 'ar']) {
|
||||
const catalog = JSON.parse(
|
||||
await readFile(
|
||||
path.join(repositoryRoot, `src/content/locales/${locale}/homepage.json`),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
if (!catalog.form || catalog.form.fleetOptions?.length !== 7) {
|
||||
failures.push(
|
||||
`${locale}: demo form fleet options do not match the approved six-value contract.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) fail(failures);
|
||||
else
|
||||
pass(
|
||||
'Phase 14 integration structure, form contract, destinations, and locale resources are present.',
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot, sha256 } from './lib.mjs';
|
||||
|
||||
const manifestName = 'PHASE_14_PACKAGE_MANIFEST_v1.0.json';
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
]);
|
||||
const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(repositoryRoot, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push(relative);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
|
||||
const failures = [];
|
||||
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 14) {
|
||||
failures.push('Phase 14 manifest identity is invalid.');
|
||||
}
|
||||
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) {
|
||||
failures.push('Manifest file count is invalid.');
|
||||
}
|
||||
const listed = new Set();
|
||||
for (const entry of manifest.files ?? []) {
|
||||
if (
|
||||
typeof entry.path !== 'string' ||
|
||||
path.isAbsolute(entry.path) ||
|
||||
entry.path.split('/').includes('..')
|
||||
) {
|
||||
failures.push(`Unsafe manifest path: ${String(entry.path)}`);
|
||||
continue;
|
||||
}
|
||||
if (listed.has(entry.path)) {
|
||||
failures.push(`${entry.path}: duplicate manifest entry.`);
|
||||
continue;
|
||||
}
|
||||
listed.add(entry.path);
|
||||
try {
|
||||
const filePath = path.join(repositoryRoot, entry.path);
|
||||
const details = await stat(filePath);
|
||||
if (!details.isFile() || details.size !== entry.bytes)
|
||||
failures.push(`${entry.path}: file metadata changed.`);
|
||||
if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`);
|
||||
} catch {
|
||||
failures.push(`${entry.path}: file is missing.`);
|
||||
}
|
||||
}
|
||||
const actual = new Set(await walk(repositoryRoot));
|
||||
for (const file of actual)
|
||||
if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
|
||||
for (const file of listed)
|
||||
if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
|
||||
if (failures.length) fail(failures);
|
||||
else pass(`Phase 14 package manifest verifies ${listed.size} repository files with no extras.`);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
const failures = [];
|
||||
const analytics = await readFile(path.join(repositoryRoot, 'src/lib/analytics/events.ts'), 'utf8');
|
||||
const client = await readFile(path.join(repositoryRoot, 'src/lib/analytics/client.ts'), 'utf8');
|
||||
const dialog = await readFile(
|
||||
path.join(repositoryRoot, 'src/components/integrations/DemoDialogHost.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
for (const forbidden of ['email', 'company', 'message', 'phone', 'fieldvalue', 'serverresponse']) {
|
||||
if (!analytics.toLowerCase().includes(`'${forbidden}'`)) {
|
||||
failures.push(`Analytics forbidden-key control omits ${forbidden}.`);
|
||||
}
|
||||
}
|
||||
if (!client.includes("dataset.analyticsConsent === 'granted'")) {
|
||||
failures.push('Analytics test adapter is not consent-gated.');
|
||||
}
|
||||
if (!client.includes("analyticsMode() !== 'test'")) {
|
||||
failures.push('Analytics adapter lacks an explicit disabled-by-default mode.');
|
||||
}
|
||||
if (/localStorage|sessionStorage/.test(dialog)) {
|
||||
failures.push('Demo form persists lead data in browser storage.');
|
||||
}
|
||||
if (/URLSearchParams|searchParams\.set/.test(dialog)) {
|
||||
failures.push('Demo form can place form data in a URL.');
|
||||
}
|
||||
|
||||
if (failures.length) fail(failures);
|
||||
else
|
||||
pass(
|
||||
'Phase 14 privacy controls prevent browser persistence, URL leakage, and unconsented analytics.',
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
const failures = [];
|
||||
const route = await readFile(path.join(repositoryRoot, 'src/app/api/demo/route.ts'), 'utf8');
|
||||
const service = await readFile(path.join(repositoryRoot, 'src/lib/demo/service.ts'), 'utf8');
|
||||
const environment = await readFile(
|
||||
path.join(repositoryRoot, 'src/lib/integrations/environment.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
for (const control of [
|
||||
"request.headers.get('origin')",
|
||||
"request.headers.get('content-type')",
|
||||
"request.headers.get('content-length')",
|
||||
"request.headers.get('x-rdg-form')",
|
||||
'maximumBodyBytes',
|
||||
'demoSubmissionEnvelopeSchema.safeParse',
|
||||
'Cache-Control',
|
||||
]) {
|
||||
if (!route.includes(control)) failures.push(`Submission API is missing control: ${control}`);
|
||||
}
|
||||
if (!service.includes('AbortController'))
|
||||
failures.push('Integration orchestration has no timeout controller.');
|
||||
if (!service.includes('executeIdempotent'))
|
||||
failures.push('Integration orchestration bypasses idempotency.');
|
||||
if (!environment.includes('DEMO_LOCAL_TEST_MODE'))
|
||||
failures.push('Loopback test escape is not explicit.');
|
||||
if (!environment.includes("['127.0.0.1', 'localhost']")) {
|
||||
failures.push('Production-like local adapter escape is not restricted to loopback origins.');
|
||||
}
|
||||
|
||||
const sourceFiles = [route, service];
|
||||
if (sourceFiles.some((source) => /console\.(log|info|debug|warn|error)\s*\(/.test(source))) {
|
||||
failures.push('Submission path writes diagnostics directly to the console.');
|
||||
}
|
||||
|
||||
if (failures.length) fail(failures);
|
||||
else
|
||||
pass(
|
||||
'Phase 14 API boundary includes origin, content-type, size, timeout, idempotency, and no-raw-log controls.',
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot, sha256 } from './lib.mjs';
|
||||
|
||||
const manifestName = 'PHASE_15_PACKAGE_MANIFEST_v1.0.json';
|
||||
const excludedDirectories = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
'__pycache__',
|
||||
]);
|
||||
const excludedFiles = new Set([
|
||||
manifestName,
|
||||
'package-lock.json',
|
||||
'tsconfig.tsbuildinfo',
|
||||
'docs/quality/phase15-command-logs/12-phase15-package-manifest.log',
|
||||
]);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(repositoryRoot, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push(relative);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
|
||||
const failures = [];
|
||||
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 15) {
|
||||
failures.push('Phase 15 manifest identity is invalid.');
|
||||
}
|
||||
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) {
|
||||
failures.push('Manifest file count is invalid.');
|
||||
}
|
||||
const listed = new Set();
|
||||
for (const entry of manifest.files ?? []) {
|
||||
if (
|
||||
typeof entry.path !== 'string' ||
|
||||
path.isAbsolute(entry.path) ||
|
||||
entry.path.split('/').includes('..')
|
||||
) {
|
||||
failures.push(`Unsafe manifest path: ${String(entry.path)}`);
|
||||
continue;
|
||||
}
|
||||
if (listed.has(entry.path)) {
|
||||
failures.push(`${entry.path}: duplicate manifest entry.`);
|
||||
continue;
|
||||
}
|
||||
listed.add(entry.path);
|
||||
try {
|
||||
const filePath = path.join(repositoryRoot, entry.path);
|
||||
const details = await stat(filePath);
|
||||
if (!details.isFile() || details.size !== entry.bytes)
|
||||
failures.push(`${entry.path}: file metadata changed.`);
|
||||
if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`);
|
||||
} catch {
|
||||
failures.push(`${entry.path}: file is missing.`);
|
||||
}
|
||||
}
|
||||
const actual = new Set(await walk(repositoryRoot));
|
||||
for (const file of actual)
|
||||
if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
|
||||
for (const file of listed)
|
||||
if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
|
||||
if (failures.length) fail(failures);
|
||||
else pass(`Phase 15 package manifest verifies ${listed.size} repository files with no extras.`);
|
||||
@@ -0,0 +1,111 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
function parseCsv(text) {
|
||||
const rows = [];
|
||||
let row = [];
|
||||
let field = '';
|
||||
let quoted = false;
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const character = text[index];
|
||||
if (quoted) {
|
||||
if (character === '"' && text[index + 1] === '"') {
|
||||
field += '"';
|
||||
index += 1;
|
||||
} else if (character === '"') {
|
||||
quoted = false;
|
||||
} else {
|
||||
field += character;
|
||||
}
|
||||
} else if (character === '"') {
|
||||
quoted = true;
|
||||
} else if (character === ',') {
|
||||
row.push(field);
|
||||
field = '';
|
||||
} else if (character === '\n') {
|
||||
row.push(field.replace(/\r$/, ''));
|
||||
rows.push(row);
|
||||
row = [];
|
||||
field = '';
|
||||
} else {
|
||||
field += character;
|
||||
}
|
||||
}
|
||||
if (quoted) throw new Error('Unterminated CSV quote.');
|
||||
if (field || row.length) {
|
||||
row.push(field.replace(/\r$/, ''));
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
for (const file of [
|
||||
'docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv',
|
||||
'docs/quality/PHASE_14_MANUAL_QA_RESULTS_v1.0.csv',
|
||||
]) {
|
||||
const rows = parseCsv(await readFile(path.join(repositoryRoot, file), 'utf8'));
|
||||
const expected = rows[0]?.length ?? 0;
|
||||
for (const [index, row] of rows.entries()) {
|
||||
if (row.length !== expected) {
|
||||
failures.push(`${file}:${index + 1} has ${row.length} columns; expected ${expected}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contentType = await readFile(
|
||||
path.join(repositoryRoot, 'src/lib/security/content-type.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const route = await readFile(path.join(repositoryRoot, 'src/app/api/demo/route.ts'), 'utf8');
|
||||
if (!contentType.includes("=== 'application/json'")) {
|
||||
failures.push('Exact JSON media-type comparison is missing.');
|
||||
}
|
||||
if (!route.includes('isApplicationJson(contentType)')) {
|
||||
failures.push('Demo API does not use the exact JSON media-type helper.');
|
||||
}
|
||||
if (route.includes("startsWith('application/json')")) {
|
||||
failures.push('Demo API still accepts look-alike JSON media types by prefix.');
|
||||
}
|
||||
|
||||
const componentVisual = await readFile(
|
||||
path.join(repositoryRoot, 'tests/visual/component-system.visual.spec.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const integrationVisual = await readFile(
|
||||
path.join(repositoryRoot, 'tests/visual/integrations.visual.spec.ts'),
|
||||
'utf8',
|
||||
);
|
||||
if (!componentVisual.includes("hpc.theme.preference")) {
|
||||
failures.push('Component visual test does not use the canonical theme storage key.');
|
||||
}
|
||||
if (componentVisual.includes('rdg-theme-preference')) {
|
||||
failures.push('Component visual test still uses the stale theme storage key.');
|
||||
}
|
||||
for (const [name, text] of [
|
||||
['component-system.visual.spec.ts', componentVisual],
|
||||
['integrations.visual.spec.ts', integrationVisual],
|
||||
]) {
|
||||
if (!text.includes("project.name !== 'chromium'")) {
|
||||
failures.push(`${name} is not restricted to canonical Chromium snapshots.`);
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = await readFile(path.join(repositoryRoot, '.github/workflows/ci.yml'), 'utf8');
|
||||
for (const command of [
|
||||
'pnpm test:integration',
|
||||
'pnpm test:security',
|
||||
'pnpm test:privacy',
|
||||
'pnpm test:browser',
|
||||
'pnpm test:a11y',
|
||||
'pnpm test:visual',
|
||||
]) {
|
||||
if (!workflow.includes(command)) failures.push(`CI omits required command: ${command}`);
|
||||
}
|
||||
|
||||
if (failures.length) fail(failures);
|
||||
else
|
||||
pass(
|
||||
'Phase 15 evidence CSVs, exact media-type control, deterministic visual setup, and CI test gates are present.',
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot, sha256 } from './lib.mjs';
|
||||
|
||||
const manifestName = 'PHASE_16_PACKAGE_MANIFEST_v1.0.json';
|
||||
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']);
|
||||
const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']);
|
||||
|
||||
async function walk(directory) {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await walk(fullPath)));
|
||||
else {
|
||||
const relative = path.relative(repositoryRoot, fullPath).split(path.sep).join('/');
|
||||
if (!excludedFiles.has(relative)) files.push(relative);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
|
||||
const failures = [];
|
||||
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16) failures.push('Phase 16 manifest identity is invalid.');
|
||||
if (manifest.release_status !== 'Launch aborted before deployment') failures.push('Manifest release status is not the recorded Phase 16 decision.');
|
||||
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) failures.push('Manifest file count is invalid.');
|
||||
const listed = new Set();
|
||||
for (const entry of manifest.files ?? []) {
|
||||
if (typeof entry.path !== 'string' || path.isAbsolute(entry.path) || entry.path.split('/').includes('..')) {
|
||||
failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue;
|
||||
}
|
||||
if (listed.has(entry.path)) { failures.push(`${entry.path}: duplicate manifest entry.`); continue; }
|
||||
listed.add(entry.path);
|
||||
try {
|
||||
const filePath = path.join(repositoryRoot, entry.path);
|
||||
const details = await stat(filePath);
|
||||
if (!details.isFile() || details.size !== entry.bytes) failures.push(`${entry.path}: file metadata changed.`);
|
||||
if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`);
|
||||
} catch { failures.push(`${entry.path}: file is missing.`); }
|
||||
}
|
||||
const actual = new Set(await walk(repositoryRoot));
|
||||
for (const file of actual) if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
|
||||
for (const file of listed) if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
|
||||
if (failures.length) fail(failures);
|
||||
else pass(`Phase 16 package manifest verifies ${listed.size} repository files with no extras.`);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { readFile, access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fail, pass, repositoryRoot } from './lib.mjs';
|
||||
|
||||
const required = [
|
||||
'docs/PHASE_16_IMPLEMENTATION_AND_LAUNCH_REPORT_v1.0.md',
|
||||
'docs/DECISION_LOG_v16.0.md',
|
||||
'docs/IMPLEMENTATION_ISSUE_REGISTER_v1.6.csv',
|
||||
'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md',
|
||||
'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md',
|
||||
'docs/phase16/FINAL_RELEASE_GATE_MATRIX_v1.0.csv',
|
||||
'docs/phase16/COMMAND_AND_PLATFORM_ACTION_MATRIX_v1.0.csv',
|
||||
'docs/phase16/DEFERRED_WORK_REGISTER_v1.0.csv',
|
||||
'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv',
|
||||
'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv',
|
||||
];
|
||||
const failures = [];
|
||||
for (const file of required) {
|
||||
try { await access(path.join(repositoryRoot, file)); }
|
||||
catch { failures.push(`Missing required Phase 16 artifact: ${file}`); }
|
||||
}
|
||||
const status = await readFile(path.join(repositoryRoot, 'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md'), 'utf8');
|
||||
const auth = await readFile(path.join(repositoryRoot, 'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md'), 'utf8');
|
||||
const flags = await readFile(path.join(repositoryRoot, 'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv'), 'utf8');
|
||||
const secrets = await readFile(path.join(repositoryRoot, 'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv'), 'utf8');
|
||||
if (!status.includes('Launch aborted before deployment')) failures.push('Final launch status is missing or inconsistent.');
|
||||
if (!auth.includes('**Rejected**')) failures.push('Launch authorization is not explicitly rejected.');
|
||||
if (/\btrue\b/.test(flags.split('\n').slice(1).map((line) => line.split(',')[1]).join('\n'))) failures.push('A Phase 16 production feature flag is enabled.');
|
||||
if (!secrets.includes('No production credentials exist') && !secrets.includes('No credential supplied')) failures.push('Secret provisioning record does not explicitly preserve missing credentials as blockers.');
|
||||
if (failures.length) fail(failures);
|
||||
else pass('Phase 16 no-go governance, disabled feature state, blocked approvals, and evidence package are explicit and internally consistent.');
|
||||
Reference in New Issue
Block a user