Skip to content

Slim plan contract and browser proof checks#124

Merged
PatrickSys merged 2 commits into
mainfrom
pivot/m1b-plan-contract
Jul 8, 2026
Merged

Slim plan contract and browser proof checks#124
PatrickSys merged 2 commits into
mainfrom
pivot/m1b-plan-contract

Conversation

@PatrickSys

@PatrickSys PatrickSys commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary:

  • Hardens browser-proof verification so required proof only closes on parser-compatible, explicitly passing observations tied to the exact plan artifact.
  • Keeps legacy no-UI plans compatible with a warning, while requiring migration for non-empty legacy UI-proof slots.
  • Aligns planner, executor, verify, template, and reference docs around runtime/test browser-proof evidence, safe local observation records, bounded claims, and privacy/safety notes.
  • Adds regressions for failed/partial proof, stale plan references, unsafe links including URL/symlink/directory/outside-workspace, and update-template preservation.

Verification:

  • rtk node --test --test-reporter=spec tests/phase.test.cjs
  • rtk node tests/gsdd.init.test.cjs
  • rtk node tests/gsdd.guards.test.cjs
  • rtk git diff --check
  • rtk npm test

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request replaces the retired machine-validated JSON UI Proof Contract with a simpler, markdown-based Browser Proof Contract. It updates the planner, executor, and verifier workflows, as well as the CLI verification logic in bin/lib/phase.mjs to parse and validate browser proof plans and observation records. Feedback on the changes highlights potential parsing failures in sectionFieldValue due to markdown formatting and inline comments, as well as a potential CLI crash in readLinkedObservationRecords when attempting to read unreadable paths or directories without error handling.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread bin/lib/phase.mjs
Comment on lines +238 to +265
function sectionFieldValue(section, label) {
const text = String(section || '');
const escapedLabel = String(label).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = new RegExp(`^\\s*(?:[-*]\\s*)?${escapedLabel}:\\s*(.*)$`, 'im').exec(text);
if (!match) return '';

const inlineValue = normalizeScalarValue(match[1]);
if (inlineValue) return inlineValue;

const nestedValues = [];
const followingLines = text.slice(match.index + match[0].length).split(/\r?\n/);
for (const line of followingLines) {
if (!line.trim()) continue;
if (/^\S/.test(line)) break;
const nestedBullet = line.match(/^\s+[-*]\s*(.+)$/);
if (nestedBullet) {
nestedValues.push(nestedBullet[1].trim());
continue;
}
const nestedText = line.match(/^\s{2,}(.+)$/);
if (nestedText) {
nestedValues.push(nestedText[1].trim());
continue;
}
break;
}
return normalizeScalarValue(nestedValues.join(' '));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of sectionFieldValue has two issues:

  1. The regular expression used to match the label (^\s*(?:[-*]\s*)?${escapedLabel}:\s*(.*)$) does not account for bold or italic markdown formatting (e.g., **Result**: or *Result*:), which is commonly used in markdown lists and explicitly shown in observation-record.md. This will cause parsing to fail for valid observation records.
  2. When parsing multi-line nested values, normalizeScalarValue is called on the joined string. If any of the nested lines contain an inline comment (e.g., path1 # comment), stripInlineComment will truncate the entire joined string from the first #, discarding all subsequent lines.

Normalizing each nested line individually and updating the regex to support optional markdown bold/italic syntax around the label resolves both issues.

function sectionFieldValue(section, label) {
  const text = String(section || '');
  const escapedLabel = String(label).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const match = new RegExp(`^\\s*(?:[-*]\\s*)?[*_]*${escapedLabel}[*_]*:\\s*(.*)$`, 'im').exec(text);
  if (!match) return '';

  const inlineValue = normalizeScalarValue(match[1]);
  if (inlineValue) return inlineValue;

  const nestedValues = [];
  const followingLines = text.slice(match.index + match[0].length).split(/\r?\n/);
  for (const line of followingLines) {
    if (!line.trim()) continue;
    if (/^\S/.test(line)) break;
    const nestedBullet = line.match(/^\s+[-*]\s*(.+)$/);
    if (nestedBullet) {
      nestedValues.push(normalizeScalarValue(nestedBullet[1]));
      continue;
    }
    const nestedText = line.match(/^\s{2,}(.+)$/);
    if (nestedText) {
      nestedValues.push(normalizeScalarValue(nestedText[1]));
      continue;
    }
    break;
  }
  return nestedValues.join(' ');
}

Comment thread bin/lib/phase.mjs
Comment on lines +423 to +437
function readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) {
const records = [];
const matcher = /^\s*(?:[-*]\s*)?(?:Browser Proof Observation|Browser proof observation|Observation record|Browser proof record):\s*(.+)$/gim;
let match;
while ((match = matcher.exec(String(summaryContent || '')))) {
const linkedPath = sanitizeLinkedObservationPath(match[1]);
if (!linkedPath) continue;
const candidates = isAbsolute(linkedPath)
? [linkedPath]
: [join(dirname(summaryFullPath), linkedPath), join(workspaceRoot, linkedPath)];
const existingPath = candidates.find((candidate) => existsSync(candidate));
if (existingPath) records.push(readFileSync(existingPath, 'utf-8'));
}
return records;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The readFileSync call is performed on existingPath without a try-catch block. Since existingPath is derived from user-provided paths in markdown files, it is untrusted and could potentially point to a directory (which existsSync returns true for) or be unreadable due to permission issues. If this happens, readFileSync will throw an unhandled exception (such as EISDIR or EACCES) and crash the CLI. Wrapping the read operation in a try-catch block prevents these crashes.

function readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) { 
  const records = [];
  const matcher = /^\s*(?:[-*]\s*)?(?:Browser Proof Observation|Browser proof observation|Observation record|Browser proof record):\s*(.+)$/gim;
  let match;
  while ((match = matcher.exec(String(summaryContent || '')))) {
    const linkedPath = sanitizeLinkedObservationPath(match[1]);
    if (!linkedPath) continue;
    const candidates = isAbsolute(linkedPath)
      ? [linkedPath]
      : [join(dirname(summaryFullPath), linkedPath), join(workspaceRoot, linkedPath)];
    const existingPath = candidates.find((candidate) => existsSync(candidate));
    if (existingPath) {
      try {
        records.push(readFileSync(existingPath, 'utf-8'));
      } catch (err) {
        // Skip unreadable files or directories safely
      }
    }
  }
  return records;
}

@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: bc819b0bb2

ℹ️ 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 bin/lib/phase.mjs Outdated
};
}

if (requiredPlans.length === 1) {

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 Require the single required plan to match its observation

When a phase has multiple plan artifacts but exactly one plan sets browser_proof_required: true, this shortcut accepts any complete Browser Proof Observation found in any matching summary, even if that observation names a different plan. Since matchingSummaries is collected phase-wide, a stale or unrelated observation from another slice can make gsdd verify report the UI-sensitive plan as verified without proof for that plan.

Useful? React with 👍 / 👎.

@PatrickSys PatrickSys changed the base branch from pivot/m1a-plan-amend to main July 8, 2026 21:09
@PatrickSys PatrickSys merged commit d40baa1 into main Jul 8, 2026
2 checks passed
@PatrickSys PatrickSys deleted the pivot/m1b-plan-contract branch July 9, 2026 05:34
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