fix: preserve opt-in GateGuard contract questions from PR 3497

This commit is contained in:
haelyra
2026-10-11 02:00:27 -04:00
12 changed files with 170 additions and 56 deletions
+8 -3
View File
@@ -1945,9 +1945,14 @@ function batchSiblingWarning(safePath) {
);
}
// see docs/gateguard/change-profile.md#external-contract-question
function questionOptions() {
return { externalContract: ECC_ENABLE_VALUES.has(normalizeEnvValue(process.env.GATEGUARD_EXTERNAL_CONTRACT_QUESTION)) };
}
function firstTouchGateMsg(filePath, isWrite, cls, profile) {
const safe = sanitizePath(filePath);
const questions = questionIdsFor(cls, isWrite, profile).map(questionText);
const questions = questionIdsFor(cls, isWrite, profile, questionOptions()).map(questionText);
return [
'[Fact-Forcing Gate]',
'',
@@ -1968,7 +1973,7 @@ function firstTouchGateMsg(filePath, isWrite, cls, profile) {
*/
function condensedGateMsg(action, filePath, ordinal, cls = 'code', targetNote = '', profile = null, missNote = '') {
const safe = sanitizePath(filePath);
const hint = condensedHintFor(cls, action === 'creation', profile);
const hint = condensedHintFor(cls, action === 'creation', profile, questionOptions());
return (
`[Fact-Forcing Gate] (denial #${ordinal} this session) First ${action} of ${safe}: ` +
`${hint} ` +
@@ -2009,7 +2014,7 @@ function firstTouchDenial(filePath, { isWrite, denials, cls, sensitive, linked =
cls,
sensitive,
profile,
questions: questionIdsFor(cls, isWrite, profile)
questions: questionIdsFor(cls, isWrite, profile, questionOptions())
});
if (denials > getFullDenialBudget()) {
const action = isWrite ? 'creation' : 'edit';
+24 -9
View File
@@ -186,7 +186,7 @@ const CLASS_QUESTIONS = Object.fromEntries(
);
// see docs/gateguard/change-profile.md#questions-from-the-change-profile
function codeQuestionIds(isWrite, profile) {
function codeQuestionIds(isWrite, profile, externalContract) {
const known = Boolean(profile) && profile.known === true;
const surface = !known || profile.touchesPublicSurface !== false;
const data = !known || profile.touchesData !== false;
@@ -194,8 +194,14 @@ function codeQuestionIds(isWrite, profile) {
// A change that handles data is usually constrained by something outside the
// tree - a consumer's dialect, a reporting timezone, a protocol's semantics -
// and the repository cannot settle it. data-schema asks what the change
// touches; external-contract asks who decides what it has to be.
return data ? [...opening, 'data-schema', 'external-contract'] : opening;
// touches; external-contract asks who decides what it has to be. It is
// opt-in: see docs/gateguard/change-profile.md#external-contract-question.
if (!data) return opening;
return externalContract ? [...opening, 'data-schema', 'external-contract'] : [...opening, 'data-schema'];
}
function asksExternalContract(options) {
return Boolean(options) && options.externalContract === true;
}
/** True when a class's questions depend on the change profile. */
@@ -203,9 +209,14 @@ function questionsUseProfile(cls) {
return !Object.hasOwn(CLASS_QUESTION_IDS, cls);
}
/** Stable ids of the first-touch questions for a class, action and change profile. */
function questionIdsFor(cls, isWrite, profile) {
const ids = Object.hasOwn(CLASS_QUESTION_IDS, cls) ? CLASS_QUESTION_IDS[cls](Boolean(isWrite)) : codeQuestionIds(Boolean(isWrite), profile);
/**
* Stable ids of the first-touch questions for a class, action and change profile.
* `options.externalContract === true` adds the opt-in external-contract question.
*/
function questionIdsFor(cls, isWrite, profile, options) {
const ids = Object.hasOwn(CLASS_QUESTION_IDS, cls)
? CLASS_QUESTION_IDS[cls](Boolean(isWrite))
: codeQuestionIds(Boolean(isWrite), profile, asksExternalContract(options));
return [...ids, 'quote-instruction'];
}
@@ -215,6 +226,8 @@ function questionText(id) {
}
const CODE_CONDENSED_HINT =
"briefly state importers/callers, affected API, data schemas if any, and the user's verbatim instruction, then retry.";
const CODE_CONDENSED_HINT_EXTERNAL_CONTRACT =
"briefly state importers/callers, affected API, data schemas if any, what outside the repository fixes the format or semantics, and the user's verbatim instruction, then retry.";
const CONDENSED_PHRASES = Object.freeze({
@@ -252,10 +265,12 @@ const CLASS_CONDENSED_HINTS = {
};
/** One-line condensed hint matching `questionIdsFor`. */
function condensedHintFor(cls, isWrite, profile) {
function condensedHintFor(cls, isWrite, profile, options) {
if (Object.hasOwn(CLASS_CONDENSED_HINTS, cls)) return CLASS_CONDENSED_HINTS[cls](Boolean(isWrite));
if (!isWrite && !(profile && profile.known === true)) return CODE_CONDENSED_HINT;
const phrases = questionIdsFor(cls, isWrite, profile).map(condensedQuestionPhrase).filter(Boolean);
if (!isWrite && !(profile && profile.known === true)) {
return asksExternalContract(options) ? CODE_CONDENSED_HINT_EXTERNAL_CONTRACT : CODE_CONDENSED_HINT;
}
const phrases = questionIdsFor(cls, isWrite, profile, options).map(condensedQuestionPhrase).filter(Boolean);
return `briefly state ${joinPhrases(phrases)}, then retry.`;
}