#!/usr/bin/env node
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p0-durable-facts-model.
// Responsibility: P1 source inventory for HWLAB#1651/#1658. It intentionally exits non-zero while read-side projection assembly remains.

import fs from "node:fs";
import path from "node:path";

const args = new Set(process.argv.slice(2));
const allowRed = args.has("--allow-red");
const json = args.has("--json");
const root = process.cwd();
const maxMatchesPerCheck = 8;

const checks = [
  {
    id: "workbench-get-result-for-trace",
    phase: "P4",
    paths: ["internal/cloud/server-workbench-http.ts", "internal/cloud/workbench-read-model.ts"],
    pattern: /resultForTrace\s*[:(]/gu,
    expectation: "GET/read-model path must not assemble turn facts from mutable Code Agent result cache."
  },
  {
    id: "workbench-get-trace-snapshot",
    phase: "P4",
    paths: ["internal/cloud/server-workbench-http.ts", "internal/cloud/workbench-read-model.ts"],
    pattern: /traceSnapshot\s*[:(]/gu,
    expectation: "GET/read-model path must read durable Workbench facts, not hydrate trace snapshots on demand."
  },
  {
    id: "compact-session-trace-result",
    phase: "P4",
    paths: ["internal/cloud/server-workbench-http.ts"],
    pattern: /compactSessionTraceResult|compactSessionTraceSnapshot/gu,
    expectation: "Session list/detail must not reconstruct trace result or snapshot from compact session payloads."
  },
  {
    id: "facts-store-runtime-read-through",
    phase: "P2",
    paths: ["internal/cloud/workbench-facts-store.ts"],
    pattern: /runtimeStore\.queryAgentTraceEvents|durableTraceSnapshot\s*\(|shouldPreferDurableTrace|mergeTraceProjectionDiagnostic/gu,
    expectation: "WorkbenchFactsStore should expose already persisted facts; runtime trace read-through belongs to projector/finalizer."
  },
  {
    id: "get-handler-projection-assembly",
    phase: "P4",
    paths: ["internal/cloud/server-workbench-http.ts"],
    pattern: /createWorkbenchTurnProjection\s*\(|sessionProjectionOptions\s*\(|sessionListProjectionOptions\s*\(/gu,
    expectation: "GET handlers should serialize a read model DTO, not run projection assembly in the request path."
  },
  {
    id: "frontend-trace-final-response-fallback",
    phase: "P5",
    paths: ["web/hwlab-cloud-web/src/composables/useTraceSubscription.ts", "web/hwlab-cloud-web/src/stores/workbench.ts"],
    pattern: /finalResponse\s*:\s*[^\n]*\?\?|finalResponseText\(result\.finalResponse\)|traceSnapshotWithTurnStatus|mergeTerminalResultTrace/gu,
    expectation: "Frontend must render sealed final facts from the server DTO, not infer final response from prior trace/result objects."
  },
  {
    id: "frontend-status-fallback",
    phase: "P5",
    paths: ["web/hwlab-cloud-web/src/composables/useTraceSubscription.ts", "web/hwlab-cloud-web/src/stores/workbench.ts"],
    pattern: /status\s*:\s*[^\n]*\?\?|\.status\s*\?\?\s*[^\n]*/gu,
    expectation: "Frontend status authority must come from the Workbench DTO/reducer, not status fallback chains."
  },
  {
    id: "probe-session-repair",
    phase: "P7",
    paths: ["scripts/web-live-dom-probe.mjs"],
    pattern: /sessionRepair/gu,
    expectation: "The final golden probe should not rely on helper-side session repair to mask default-route/read-model gaps."
  }
];

const findings = [];
const missingFiles = [];

for (const check of checks) {
  const matches = [];
  for (const relativePath of check.paths) {
    const filePath = path.resolve(root, relativePath);
    if (!fs.existsSync(filePath)) {
      missingFiles.push(relativePath);
      continue;
    }
    const source = fs.readFileSync(filePath, "utf8");
    const lineStarts = lineStartOffsets(source);
    for (const match of source.matchAll(check.pattern)) {
      const line = lineNumberForOffset(lineStarts, match.index ?? 0);
      const snippet = source.split("\n")[line - 1]?.trim() ?? "";
      matches.push({ path: relativePath, line, text: match[0], snippet });
      if (matches.length >= maxMatchesPerCheck) break;
    }
  }
  if (matches.length > 0) findings.push({ ...check, pattern: String(check.pattern), matches });
}

const report = {
  ok: findings.length === 0 && missingFiles.length === 0,
  status: findings.length > 0 ? "red" : missingFiles.length > 0 ? "blocked" : "green",
  issue: "pikasTech/HWLAB#1658",
  parentIssue: "pikasTech/HWLAB#1651",
  checkedAt: new Date().toISOString(),
  checkedFiles: [...new Set(checks.flatMap((check) => check.paths))],
  missingFiles,
  redFindingCount: findings.length,
  findings,
  nextPhases: [...new Set(findings.map((finding) => finding.phase))].sort(),
  valuesPrinted: false
};

if (json) {
  console.log(JSON.stringify(report, null, 2));
} else {
  printTextReport(report);
}

if (!report.ok && !allowRed) process.exit(1);

function printTextReport(report) {
  console.log(`Workbench P1 inventory: ${report.status}`);
  console.log(`redFindingCount=${report.redFindingCount} missingFiles=${report.missingFiles.length}`);
  if (report.missingFiles.length > 0) console.log(`missingFiles=${report.missingFiles.join(",")}`);
  for (const finding of report.findings) {
    console.log(`\n[${finding.phase}] ${finding.id}`);
    console.log(`expectation: ${finding.expectation}`);
    for (const match of finding.matches) {
      console.log(`- ${match.path}:${match.line}: ${match.snippet}`);
    }
  }
  if (report.findings.length > 0) {
    console.log("\nThis is the expected P1 red inventory. Use --allow-red when collecting evidence without failing the shell.");
  }
}

function lineStartOffsets(source) {
  const starts = [0];
  for (let index = 0; index < source.length; index += 1) {
    if (source.charCodeAt(index) === 10) starts.push(index + 1);
  }
  return starts;
}

function lineNumberForOffset(starts, offset) {
  let low = 0;
  let high = starts.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (starts[mid] <= offset) low = mid + 1;
    else high = mid - 1;
  }
  return Math.max(1, high + 1);
}
