Skip to content

feat: add investigator scan vertical slice - #34

Open
devjusty wants to merge 2 commits into
mainfrom
feat/r1-investigator-vertical-slice
Open

devjusty wants to merge 2 commits into
mainfrom
feat/r1-investigator-vertical-slice

Conversation

@devjusty

@devjusty devjusty commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add canonical R1 investigation/session contracts and progressive identity, exposure, and actionable finding layers.
  • Add authenticated persistence/resume, browser-local anonymous continuity, explicit claim/import, retry, cancellation, and idempotent claim race protection.
  • Preserve legacy scan behavior while mapping production WordPress results into canonical identity/exposure/findings.
  • Add server-side domain normalization, canonical response parsing, interrupted-session recovery, dependency propagation, evidence normalization, and canonical recent-domain rescans.
  • Add focused contract, frontend, server, integration, plan, and validation documentation.

Verification

  • pnpm typecheck passed.
  • Targeted frontend: 154 tests passed.
  • Targeted server route/investigation tests: 19 tests passed; broader server execution remains blocked by Jest ESM configuration.
  • Contracts: 35 tests passed.
  • Frontend and server lint passed.
  • Frontend production build passed with existing chunk-size warning.
  • git diff --check passed.

Known limitations

  • R1 gate remains NOT PASSED: four authorized live walkthroughs are pending.
  • Browser Vitest tests remain blocked by missing Playwright Chromium and mixed Vitest/browser versions.
  • Full frontend fallback has two unrelated existing AdminPage failures.
  • Remote Turso contention and migration behavior remain untested.
  • Fallow complexity hotspots remain in ScanPage.jsx, SitemapSection.jsx, and ExposurePanel.jsx.

Next steps

  1. Run authorized walkthroughs for healthy, plugin-heavy, partially blocked/auth-restricted, and non-WordPress domains.
  2. Verify remote Turso migration and contention behavior.
  3. Repair browser/server test infrastructure and rerun blocked suites.
  4. Refactor complexity hotspots and address remaining malformed-record/key hardening.
  5. Reassess validation evidence before passing R1.

@netlify

netlify Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploy Preview for wp-json-discovery ready!

Name Link
🔨 Latest commit 26e1a4f
🔍 Latest deploy log https://app.netlify.com/projects/wp-json-discovery/deploys/6aa3af8e58e3bf0008b1828c
😎 Deploy Preview https://deploy-preview-34--wp-json-discovery.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ff852de28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/src/api/client.js Outdated
Comment on lines +330 to +331
const record = investigationRecordSchema.safeParse(envelope.data.data);
if (!record.success) throw new Error('Invalid investigation response');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse session updates with the session-record schema

requestInvestigation parses every successful response as an investigationRecord, but POST /api/investigations/:id/sessions/:sessionId returns the sessionRecord produced by saveInvestigationSession (server/src/routes/investigations.js:65-68). Consequently every authenticated progress write succeeds in the database and then throws Invalid investigation response; ScanPage reports a persistence failure and creates an anonymous fallback, which can lead users to import a duplicate investigation.

Useful? React with 👍 / 👎.

Comment on lines +78 to +82
const snapshot = loadAnonymousInvestigation();
if (snapshot) {
setAnonymousSnapshot(snapshot);
if (!isAuthenticated) {
setInvestigatorSession(hydrateSession(snapshot.record.session, snapshot.domain, scanSettings.options));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recover active snapshots instead of restoring them as running

If the page reloads while an anonymous scan is queued or running, this effect only hydrates that persisted state and never restarts or converts it to a recoverable failure. The restored status keeps the Start scan button disabled at line 349, while retry controls are offered only for terminal capability states, leaving the investigator permanently stuck; the authenticated resume path at lines 97-102 has the same behavior.

Useful? React with 👍 / 👎.

Comment on lines +80 to +85
function IdentityLayer({ result }) {
const identity = result?.identity;
const evidence = normalizeEvidence(identity?.evidence, identity);
const hasValue = typeof identity?.value === 'string' && identity.value.trim().length > 0;
const status = hasValue ? evidenceStatus(evidence.successful) : 'unavailable';
const value = status === 'unavailable' ? 'Unavailable' : identity.value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Project real scan output into the identity and findings layers

The production WordPress runner remains scanDomain, whose returned object contains domain, summary, exposure, performance, plugins, and metrics, but no identity or findings; repository-wide search finds producers for these fields only in test fixtures. Therefore every real successful scan reaches this code with identity undefined, renders the Site profile as unavailable, and never renders actionable findings, defeating the new core result hierarchy.

AGENTS.md reference: AGENTS.md:L7-L9

Useful? React with 👍 / 👎.

Comment on lines +53 to +56
{['failed', 'unavailable'].includes(capability.status) && capability.outcome?.error?.retryable ? (
<Button type="button" variant="secondary" size="sm" disabled={retryingCapabilityId === id} onClick={() => onRetryCapability(id)}>
{retryingCapabilityId === id ? `Retrying ${CAPABILITY_LABELS[id] ?? id}…` : `Retry ${CAPABILITY_LABELS[id] ?? id}`}
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expose a retry action for unavailable capabilities

When a capability is unavailable because its runner was absent or a dependency failed, the contract requires its outcome error to have retryable: false, so this condition can never render a retry button for any unavailable state. After the runner becomes available or the failed dependency is successfully retried, users therefore cannot invoke the engine's supported unavailable-capability retry path without starting a new investigation.

Useful? React with 👍 / 👎.

Comment on lines 389 to +395
<ScanSectionContent
activeSection={visibleSection}
session={session}
session={investigatorSession ? bridgeInvestigatorSession(investigatorSession) : session}
scanSettings={scanSettings}
onScanSettingsChange={updateScanSettings}
onRunCapability={runCapability}
onRetryCapability={retryCapability}
onRunCapability={investigatorSession ? handleRunInvestigatorCapability : runCapability}
onRetryCapability={investigatorSession ? handleRetryInvestigatorCapability : retryCapability}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep recent-domain rescans on the visible session path

Recent-domain cards still invoke the legacy startScan callback, but after an investigator session has been loaded or completed this conditional always renders investigatorSession instead of the newly updated legacy session. An authenticated user clicking Rescan therefore runs network work while the status stack, sidebar, and results continue showing the previous investigation; clear/replace investigator state or route rescans through the new controller.

Useful? React with 👍 / 👎.

setInvestigatorSession(hydrated);
void persistInvestigatorSession(hydrated, identity);
};
const result = await runInvestigationSession(hydratedNextSession, getCapabilityRunners(selection.capabilityIds), onChange, token);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invoke capability settlement hooks from the new scan path

The new primary form path passes bare runners directly to runInvestigationSession, bypassing useScan.invokeSettledOutcomes. As a result, WordPress's registered onWordpressSettled hook never persists newly detected unsupported namespaces, invalidates related queries, or emits scan.complete/scan.error, so new investigator scans stop populating the existing unsupported-plugin and scan-history workflows.

AGENTS.md reference: AGENTS.md:L82-L84

Useful? React with 👍 / 👎.

Comment on lines +25 to +29
export function saveAnonymousInvestigation(snapshot) {
const current = loadAnonymousInvestigation();
const next = normalizeSnapshot(snapshot);
if (!next) return;
if (current && Date.parse(current.record.persistedAt) >= Date.parse(next.record.persistedAt)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace equal-timestamp snapshots when the state advances

ScanPage timestamps every emitted snapshot with millisecond-resolution new Date().toISOString(), while the engine publishes a failed capability state and its finalized failed session synchronously. When those writes share a millisecond, this >= check discards the terminal snapshot and retains the preceding running state; the next reload then restores a scan that can never finish. Preserve ordering with a revision/sequence or allow an equal-timestamp terminal state to supersede progress.

Useful? React with 👍 / 👎.

Comment on lines +110 to +115
function ActionableFindings({ findings }) {
const supportedFindings = [];
const unavailableFindings = [];
(findings ?? []).forEach((finding) => {
const evidence = normalizeEvidence(finding?.evidence, finding);
if (evidence.successful.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Interpret findings using the canonical evidence contract

For a contract-valid findingSchema value, finding.evidence contains references shaped as { id, capabilityId, locator }, while confidence is stored in finding.evidenceLevel; none of those references has the status field expected by normalizeEvidence. Thus every canonical finding is classified as having no successful evidence and is moved to the unavailable section. Use evidenceLevel to classify the finding and use the references for provenance rather than requiring the test-only evidence shape.

Useful? React with 👍 / 👎.

Comment thread server/src/routes/investigations.js Outdated
Comment on lines +47 to +50
router.post('/', wrapAsync(async (req, res) => {
const userId = requireUser(req);
const input = validate(startInvestigationRequestSchema, req.body);
const record = await createInvestigation(userId, input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate and normalize domains at the server boundary

For direct API clients, the start route only applies startInvestigationRequestSchema, whose domain fields merely require nonempty strings, and never invokes the existing sanitizeDomain boundary. Requests can therefore persist malformed, private, or noncanonical identities such as localhost, IP literals, or caller-supplied normalized values that disagree with the submitted domain; the claim route has the same gap. Normalize from the submitted value server-side and reject values the repository sanitizer rejects before writing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant