#!/usr/bin/env bash
# check-doc-drift.sh — minimum structure-doc drift gate for agbrowse.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

node <<'NODE'
const fs = require('fs');

let failures = 0;
let passes = 0;

function pass(message) {
  console.log(`PASS ${message}`);
  passes += 1;
}

function fail(message) {
  console.error(`FAIL ${message}`);
  failures += 1;
}

function read(file) {
  return fs.readFileSync(file, 'utf8');
}

const requiredFiles = [
  'structure/AGENTS.md',
  'structure/INDEX.md',
  'structure/str_func.md',
  'structure/commands.md',
  'structure/runtime_contracts.md',
  'structure/release_gates.md',
  'structure/phase_status.md',
  'structure/verify-counts.sh',
  'structure/_legacy/.gitkeep',
  'docs/production-readiness.md',
  'docs/comparison.md',
  'docs/benchmarks.md',
  'benchmarks/agbrowse/trajectory.mjs',
  'benchmarks/agbrowse/run-task.mjs',
];

for (const file of requiredFiles) {
  if (fs.existsSync(file)) pass(`${file} exists`);
  else fail(`${file} is missing`);
}

const commandsDoc = read('structure/commands.md');
const runtimeDoc = read('structure/runtime_contracts.md');
const releaseDoc = read('structure/release_gates.md');
const phaseStatusDoc = read('structure/phase_status.md');
const indexDoc = read('structure/INDEX.md');
const readme = read('README.md');
const productionDoc = read('docs/production-readiness.md');
const comparisonDoc = read('docs/comparison.md');
const benchmarksDoc = read('docs/benchmarks.md');
const pkg = JSON.parse(read('package.json'));

const semanticContractFiles = [
  'structure/CAPABILITY_TRUTH_TABLE.md',
  'structure/phase_status.md',
  'structure/commands.md',
  'structure/runtime_contracts.md',
  'structure/release_gates.md',
  'skills/browser/browser.mjs',
  'skills/web-ai/SKILL.md',
  'README.md',
  'docs/dev/index.html',
  'docs/dev/quickstart.html',
  'docs/dev/guides/web-ai.html',
  'docs/dev/guides/code-mode.html',
  'docs/dev/reference/cli.html',
  'docs/dev/reference/release-gates.html',
  'docs/dev/ko/index.html',
  'docs/dev/ko/quickstart.html',
  'docs/dev/ko/guides/web-ai.html',
  'docs/dev/ko/guides/code-mode.html',
  'docs/dev/ko/reference/cli.html',
  'docs/dev/ko/reference/release-gates.html',
].filter(f => fs.existsSync(f));

const staleChatGptDocTokens = [
  'Pro Extended',
  'Pro Standard',
  'Pro \uD655\uC7A5',
  'model-switcher-gpt-5-5',
  'model-switcher-dropdown-button',
  'composer-intelligence-pro-thinking-effort-trigger',
  'Pro: standard/extended',
  'Thinking: light/standard/extended/heavy',
  '--effort light',
  '--effort standard',
  '--effort extended',
  '--effort heavy',
  '--timeout 1800',
];

const requiredSemanticTokensByFile = new Map([
  ['skills/browser/browser.mjs', [
    '--surface <chat>',
    '--family <alias>',
    'gpt-5.6-sol',
    'Thinking canonical: medium/high/xhigh',
    'chatgpt-pro=5400',
    'grok-heavy=3600',
    'deep-research=3600',
  ]],
]);

function lineNumberAt(source, offset) {
  return source.slice(0, offset).split('\n').length;
}

function lineAt(source, offset) {
  const start = source.lastIndexOf('\n', offset - 1) + 1;
  const end = source.indexOf('\n', offset);
  return source.slice(start, end === -1 ? source.length : end);
}

function isLegacyUiOccurrence(file, source, offset) {
  const marker = 'Legacy UI (before 2026-07-10)';
  if (file === 'README.md') return lineAt(source, offset).includes(marker);
  if (file !== 'skills/web-ai/SKILL.md') return false;

  const start = source.lastIndexOf('### ' + marker, offset);
  if (start === -1) return false;
  const ends = [
    source.indexOf('\nGemini:', start),
    source.indexOf('\n## ', start + marker.length),
  ].filter(candidate => candidate !== -1);
  const end = ends.length > 0 ? Math.min(...ends) : source.length;
  return offset < end;
}

function isAllowedStaleOccurrence(file, token, source, offset) {
  if (token === '--timeout 1800') {
    const deepResearchExampleFiles = [
      'skills/web-ai/SKILL.md',
      'README.md',
      'docs/dev/guides/web-ai.html',
      'docs/dev/ko/guides/web-ai.html',
    ];
    if (!deepResearchExampleFiles.includes(file)) return false;
    return source.slice(Math.max(0, offset - 240), offset).includes('--research deep');
  }
  if (token.startsWith('--effort ')) {
    return /(?:Legacy|legacy|compatibility)/.test(lineAt(source, offset));
  }
  return isLegacyUiOccurrence(file, source, offset);
}

for (const file of semanticContractFiles) {
  const source = read(file);
  const violations = [];
  for (const token of staleChatGptDocTokens) {
    let offset = source.indexOf(token);
    while (offset !== -1) {
      if (!isAllowedStaleOccurrence(file, token, source, offset)) {
        violations.push(JSON.stringify(token) + ' at line ' + lineNumberAt(source, offset));
      }
      offset = source.indexOf(token, offset + token.length);
    }
  }
  for (const token of requiredSemanticTokensByFile.get(file) || []) {
    if (!source.includes(token)) {
      violations.push('missing required current token ' + JSON.stringify(token));
    }
  }
  if (violations.length > 0) {
    fail(file + ' has ChatGPT semantic doc-contract violations: ' + violations.join(', '));
  } else {
    pass(file + ' has current required tokens and no unallowlisted stale ChatGPT doc-contract tokens');
  }
}


const rootCommands = [
  'start', 'stop', 'status', 'reset',
  'snapshot', 'screenshot', 'text', 'get-dom',
  'click', 'type', 'press', 'hover', 'select', 'check', 'uncheck', 'drag',
  'mouse-click', 'move-mouse', 'mouse-down', 'mouse-up',
  'navigate', 'reload', 'resize', 'tabs', 'tab-switch', 'select-tab',
  'tab-cleanup', 'scroll',
  'wait', 'wait-for-selector', 'wait-for-text', 'wait-for',
  'console', 'network', 'evaluate',
  'web-ai', 'runway', 'skills', 'install-skills',
];

const runwayCommands = [
  'selectors', 'status', 'open', 'preflight', 'poll',
];

const webAiCommands = [
  'render', 'status', 'send', 'poll', 'query', 'code', 'code-extract',
  'stop', 'watch', 'snapshot', 'project-sources list/add',
  'sessions list', 'sessions show', 'sessions resume',
  'sessions reattach', 'sessions prune', 'context-dry-run',
  'context-render', 'mcp-server', 'eval', 'doctor', 'claim-audit',
];

const mcpTools = [
  'browser_snapshot', 'browser_click_ref',
  'web_ai_snapshot', 'web_ai_click_ref', 'web_ai_submit_prompt',
  'web_ai_wait_response', 'web_ai_copy_markdown', 'web_ai_doctor',
  'web_ai_session_resume',
];

for (const command of rootCommands) {
  if (commandsDoc.includes(`\`${command}\``)) pass(`commands.md lists root command ${command}`);
  else fail(`commands.md missing root command ${command}`);
}

for (const command of webAiCommands) {
  if (commandsDoc.includes(`\`${command}\``)) pass(`commands.md lists web-ai command ${command}`);
  else fail(`commands.md missing web-ai command ${command}`);
}

for (const command of runwayCommands) {
  if (commandsDoc.includes(`\`${command}\``)) pass(`commands.md lists runway command ${command}`);
  else fail(`commands.md missing runway command ${command}`);
}

for (const tool of mcpTools) {
  if (commandsDoc.includes(`\`${tool}\``)) pass(`commands.md lists MCP tool ${tool}`);
  else fail(`commands.md missing MCP tool ${tool}`);
}

const expectedScripts = [
  'test', 'test:unit', 'test:integration', 'test:eval',
  'test:contract-drift', 'test:trace-policy', 'test:mcp',
  'test:source-audit', 'test:release-gates', 'benchmark:trajectory',
  'release', 'release:preview',
];

for (const scriptName of expectedScripts) {
  if (pkg.scripts && pkg.scripts[scriptName]) pass(`package.json has ${scriptName}`);
  else fail(`package.json missing ${scriptName}`);
  if (releaseDoc.includes(`npm run ${scriptName}`) || releaseDoc.includes(`npm ${scriptName}`) || releaseDoc.includes(`\`${scriptName}\``)) {
    pass(`release_gates.md mentions ${scriptName}`);
  } else {
    fail(`release_gates.md missing ${scriptName}`);
  }
}

if (Array.isArray(pkg.files) && pkg.files.includes('structure/')) pass('package files include structure/');
else fail('package files missing structure/');
if (Array.isArray(pkg.files) && pkg.files.includes('docs/')) pass('package files include docs/');
else fail('package files missing docs/');
if (Array.isArray(pkg.files) && pkg.files.includes('benchmarks/')) pass('package files include benchmarks/');
else fail('package files missing benchmarks/');

if (readme.includes('structure/INDEX.md')) pass('README links structure/INDEX.md');
else fail('README missing structure/INDEX.md link');

for (const label of ['Ready surfaces', 'Beta surfaces', 'Experimental or deferred surfaces']) {
  if (readme.includes(label)) pass(`README includes ${label}`);
  else fail(`README missing ${label}`);
}

for (const docCheck of [
  [productionDoc, 'Ready', 'production-readiness.md labels ready surfaces'],
  [productionDoc, 'Beta', 'production-readiness.md labels beta surfaces'],
  [comparisonDoc, 'Comparison Rules', 'comparison.md has comparison rules'],
  [benchmarksDoc, 'Claim Boundary', 'benchmarks.md has claim boundary'],
]) {
  if (docCheck[0].includes(docCheck[1])) pass(docCheck[2]);
  else fail(docCheck[2]);
}

for (const linked of ['str_func.md', 'commands.md', 'runtime_contracts.md', 'release_gates.md', 'phase_status.md', 'check-doc-drift.sh', 'verify-counts.sh']) {
  if (indexDoc.includes(linked)) pass(`INDEX links ${linked}`);
  else fail(`INDEX missing ${linked}`);
}

for (const statusNeedle of ['18 MCP/AI SDK', '19 remote CDP adapters', '20 benchmark trajectory', '21 release gates']) {
  if (phaseStatusDoc.includes(statusNeedle)) pass(`phase_status.md tracks ${statusNeedle}`);
  else fail(`phase_status.md missing ${statusNeedle}`);
}

for (const forbiddenClaim of ['No stealth', 'No leaderboard score', 'No production MCP claim beyond']) {
  if (phaseStatusDoc.includes(forbiddenClaim)) pass(`phase_status.md blocks ${forbiddenClaim}`);
  else fail(`phase_status.md missing forbidden claim guard: ${forbiddenClaim}`);
}

for (const phase of [
  '13_phase12_trace_replay.md',
  '14_phase13_safety_policy.md',
  '15_phase14_active_command_ownership.md',
  '18_phase17_provider_contracts_source_audit.md',
  '19_phase18_mcp_ai_sdk_hardening.md',
  '22_phase21_release_gates.md',
]) {
  if (runtimeDoc.includes(phase)) pass(`runtime_contracts.md links ${phase}`);
  else fail(`runtime_contracts.md missing ${phase}`);
}

if (failures > 0) {
  console.error(`\n${failures} drift check(s) failed; ${passes} passed.`);
  process.exit(1);
}

console.log(`\nAll structure drift checks passed (${passes}).`);
NODE
