* test(#2801): failing-first suite for the hostBehaviors.reviewerCli alias removal Inverts the Phase 5a rows that assert the derived legacy alias still contributes a reviewer slug, and adds the removal-warning coverage the alias's exit needs (ADR-2782 D9). RED against unmodified production code, by design: the six shipped manifests still declare the key and collectReviewerWarnings emits nothing for hostBehaviors. Refs #2801 * chore(#2801): remove the hostBehaviors.reviewerCli deprecated alias ADR-2782 D9, Phase 7 — the final phase of epic #2782. The derived legacy alias survived one release (Phase 5a shipped in 1.9.0; 1.9.1 and 1.10.0 have since gone out), so it goes. A declared reviewer body is now the only route onto the reviewer roster. - deriveReviewerSlugs no longer reads runtime.hostBehaviors.reviewerCli - the key is stripped from the six manifests that carried it; each already declares a reviewer body whose slug equals its capability id, so the derived roster is unchanged at the same twelve slugs - collectReviewerWarnings emits a presence-based, non-fatal removal notice for any manifest still declaring the key, reaching both the build-time registry generation and the third-party overlay load path. The check runs before the reviewer-body early-return, because the manifest it exists for is the alias-only one that has no body. - hostBehaviors stays an open, unvalidated bag for its other 59 keys; this adds one keyed removal notice, not general validation Refs #2801 * refactor(#2801): give the reviewer-warning channel a typed IR Review finding: the new tests asserted with String#includes() on the warning prose, which CONTRIBUTING.md's 'Prohibited: Raw Text Matching on Test Outputs' bans in favor of a typed intermediate representation. Adds the IR beside the renderer rather than replacing it, which is the shape that section prescribes and bin/verify-reapply-patches.cjs already models: - REVIEWER_WARNING, a frozen code enum - REMOVED_REVIEWER_CLI_FIELD, so the emitting site and its test share one symbol instead of duplicating a literal - collectReviewerWarningRecords(cap), returning typed records collectReviewerWarnings(cap) keeps its exact string[] contract as a thin map over the records, so both production consumers are untouched. Every section-K row now asserts on record.code/field/capId and none on the rendered message. Locks the code surface, asserts the renderer stays one-to-one with the records, and migrates the pre-existing Phase 2 test on the same channel off prose matching. Refs #2801 * test(#2801): invert the section F alias fall-through regression row Caught by the remote runner: 2 unique failures on both Node lanes out of 31,692. tests/reviewer-lane-declarations.test.cjs section F — Phase 5a's isolated-security-review regressions — asserted that a blank reviewer.slug falls through to the hostBehaviors.reviewerCli alias rather than dropping the lane. That is the direct inverse of this phase's contract. The original rationale held only while the alias existed. With it gone there is nothing to fall through to: a blank body is not a declaration, and a declaration is the only route onto the roster. Inverted rather than deleted — the row carries the adversarial-review provenance for the slug trim, and removing a security regression guard to make a change pass is backwards. The duplicate row added earlier in section C is dropped instead; section F is its canonical home. Also corrects two count strings Phase 5b left at eleven while asserting twelve, which would misreport on failure. Refs #2801 * docs(#2801): give the removed reviewerCli flag a migration path The Reference edit alone satisfied CI — a file under docs/ moved, so lint-docs-required.cjs was green — while the task-oriented quadrant said nothing about the removal. A maintainer whose lane had just gone silent would have found the field documented as removed and no page telling them what to do about it. Adds a migration section to the how-to: the symptom, the verbatim warning they will see, the before/after manifest, and the note to keep the reviewer slug equal to the capability id so existing review.default_reviewers entries and --<slug> flags survive. Refs #2801 * chore(#2801): backfill changeset pr number to 3272 * feat(#2801): close the runtime.hostBehaviors vocabulary ADR-1016 closes twelve descriptor axes and rejects an open escape hatch in the descriptor. It never mentioned runtime.hostBehaviors, and that silence was read as permission: 59 keys across 18 manifests, 39 of them set by a single capability, validated by nothing. The reference docs went further and attributed the open seam to ADR-1016, which does not mention the field at all. KNOWN_HOST_BEHAVIORS enumerates the vocabulary. An undeclared key yields a non-fatal UNKNOWN_HOST_BEHAVIOR record on the same D4.3 channel as the alias removal notice, reaching both build-time generation and overlay install. Warning, never error, for the reason this phase exists: an error would hard-break an out-of-tree descriptor carrying a bespoke key with no deprecation window, which is what reviewerCli was given a release to avoid. Escalation is a separate decision. reviewerCli is excluded from the unknown-key sweep so it keeps its own notice with the migration pointer rather than drawing two records. A parity test binds the vocabulary to the shipped manifests in both directions, and a second asserts no shipped capability draws a notice, so the closure is provably inert in-tree. Records the decision and the miscitation as an ADR-1016 amendment. Refs #2801 * fix(#2801): bound and sanitize the unknown-key diagnostics Two findings from an isolated adversarial review of the closure commit, both proven by execution rather than asserted. MAJOR, introduced by the closure: the new Object.keys(hostBehaviors) sweep had no ceiling. An installed third-party manifest is bounded only by MANIFEST_MAX_BYTES, and an 8.69MB manifest with 800,000 keys produced 800,000 records and ~139MB of message text, retained for the registry's lifetime in OverlayMeta.diagnostics. Now capped at ten records plus a summary carrying omittedCount, mirroring capability-loader's existing slice(0,3) idiom. The same manifest now yields 11 records and 1748 chars. MINOR, newly reachable: manifest-supplied key names were interpolated raw. Unlike cap.id, which validateCapability gates on KEBAB_RE before these diagnostics run, hostBehaviors keys have no grammar check anywhere, so ANSI escapes and CRLF reached stderr and OverlayMeta.warnings intact. New describeKey replaces C0/C1 controls and clips at 80 chars. The file already had describeValue for this and applied it only to values. Both fixes land on the pre-existing reviewer.* sweep too — it carried the identical pair, and fixing only the new copy would leave the same defect one screen from its own fix. Refs #2801 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -187,7 +187,6 @@ const capabilities = {
|
||||
"effortSurface": "undocumented"
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"reviewerCli": true,
|
||||
"projectInstructionFile": "GEMINI.md",
|
||||
"noPathRewrite": true,
|
||||
"hookPathStyle": "raw",
|
||||
@@ -577,8 +576,7 @@ const capabilities = {
|
||||
"skillsGlobalOnboarding": true,
|
||||
"legacyCommandsGsdInstallMigration": true,
|
||||
"legacyCommandsGsdUninstall": "global",
|
||||
"hyphenNameAgentBody": true,
|
||||
"reviewerCli": true
|
||||
"hyphenNameAgentBody": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -1085,8 +1083,7 @@ const capabilities = {
|
||||
"tomlConfigInstall": true,
|
||||
"cleanupSkillSidecars": true,
|
||||
"agentTomlFiles": true,
|
||||
"frontmatterDialect": "codex",
|
||||
"reviewerCli": true
|
||||
"frontmatterDialect": "codex"
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -1337,8 +1334,7 @@ const capabilities = {
|
||||
"stop",
|
||||
"subagentStart",
|
||||
"subagentStop"
|
||||
],
|
||||
"reviewerCli": true
|
||||
]
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -2661,8 +2657,7 @@ const capabilities = {
|
||||
"skipHomePrefixSubstitution": true,
|
||||
"skipSettingsUi": true,
|
||||
"skipUpdateBannerCommand": true,
|
||||
"skipCodexSkillsManifest": true,
|
||||
"reviewerCli": true
|
||||
"skipCodexSkillsManifest": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -3005,8 +3000,7 @@ const capabilities = {
|
||||
"legacyCommandsGsdCleanup": true,
|
||||
"legacyCommandsGsdInstallMigration": true,
|
||||
"legacyCommandsGsdUninstall": true,
|
||||
"hyphenNameAgentBody": true,
|
||||
"reviewerCli": true
|
||||
"hyphenNameAgentBody": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -4853,7 +4847,6 @@ const runtimes = {
|
||||
"effortSurface": "undocumented"
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"reviewerCli": true,
|
||||
"projectInstructionFile": "GEMINI.md",
|
||||
"noPathRewrite": true,
|
||||
"hookPathStyle": "raw",
|
||||
@@ -5114,8 +5107,7 @@ const runtimes = {
|
||||
"skillsGlobalOnboarding": true,
|
||||
"legacyCommandsGsdInstallMigration": true,
|
||||
"legacyCommandsGsdUninstall": "global",
|
||||
"hyphenNameAgentBody": true,
|
||||
"reviewerCli": true
|
||||
"hyphenNameAgentBody": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -5431,8 +5423,7 @@ const runtimes = {
|
||||
"tomlConfigInstall": true,
|
||||
"cleanupSkillSidecars": true,
|
||||
"agentTomlFiles": true,
|
||||
"frontmatterDialect": "codex",
|
||||
"reviewerCli": true
|
||||
"frontmatterDialect": "codex"
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -5683,8 +5674,7 @@ const runtimes = {
|
||||
"stop",
|
||||
"subagentStart",
|
||||
"subagentStop"
|
||||
],
|
||||
"reviewerCli": true
|
||||
]
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -6264,8 +6254,7 @@ const runtimes = {
|
||||
"skipHomePrefixSubstitution": true,
|
||||
"skipSettingsUi": true,
|
||||
"skipUpdateBannerCommand": true,
|
||||
"skipCodexSkillsManifest": true,
|
||||
"reviewerCli": true
|
||||
"skipCodexSkillsManifest": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
@@ -6477,8 +6466,7 @@ const runtimes = {
|
||||
"legacyCommandsGsdCleanup": true,
|
||||
"legacyCommandsGsdInstallMigration": true,
|
||||
"legacyCommandsGsdUninstall": true,
|
||||
"hyphenNameAgentBody": true,
|
||||
"reviewerCli": true
|
||||
"hyphenNameAgentBody": true
|
||||
}
|
||||
},
|
||||
"reviewer": {
|
||||
|
||||
@@ -1578,6 +1578,122 @@ const KNOWN_REVIEWER_FIELDS = new Set([
|
||||
'handler',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The closed `runtime.hostBehaviors` vocabulary (ADR-1016, closed via #2801).
|
||||
*
|
||||
* ADR-1016's core principle is that every per-runtime difference is a value over
|
||||
* a closed primitive vocabulary, and that a host needing a new shape gets a
|
||||
* reviewed first-party primitive rather than "an open escape hatch in the
|
||||
* descriptor" (§Alternatives #2 rejects exactly that). `hostBehaviors` was the
|
||||
* one hole left in that closure: 59 keys across 18 manifests, 39 of them set by
|
||||
* a single capability, validated by nothing. It was described in the reference
|
||||
* docs as a deliberate open seam sanctioned by ADR-1016 — ADR-1016 does not
|
||||
* mention `hostBehaviors` at all, and its stated principle is the opposite.
|
||||
*
|
||||
* Adding a key here is deliberate and reviewed. That friction IS the decision
|
||||
* (ADR-1016 §Consequences: "the closed vocabulary must grow (reviewed) when a
|
||||
* genuinely new host shape appears — intentional friction, the trust boundary").
|
||||
*
|
||||
* WARNING, never error. Two reasons:
|
||||
* 1. It matches the ADR-2782 D4.3 treatment of an unknown `reviewer` field, so
|
||||
* a manifest built against a newer GSD degrades visibly instead of failing
|
||||
* the build of a repo that merely reads it.
|
||||
* 2. An error would hard-break an out-of-tree descriptor carrying a bespoke
|
||||
* key, with no deprecation window — the exact mistake #2801's own alias
|
||||
* removal spent a full release avoiding. Escalating to an error is a later
|
||||
* step and needs its own window.
|
||||
*
|
||||
* Kept in sorted order, and `tests/reviewer-manifest-body.test.cjs` asserts this
|
||||
* set equals the keys the shipped manifests actually declare, so the list cannot
|
||||
* silently rot away from reality (DEFECT.GENERATIVE-FIX).
|
||||
*/
|
||||
const KNOWN_HOST_BEHAVIORS = new Set([
|
||||
'agentFileExtension',
|
||||
'agentFrontmatterExtensions',
|
||||
'agentManifestStyle',
|
||||
'agentTomlFiles',
|
||||
'attributionConfigResolver',
|
||||
'attributionSource',
|
||||
'authorsCanonicalWorkflow',
|
||||
'brandingRewrites',
|
||||
'cleanupSkillSidecars',
|
||||
'clineRulesSurface',
|
||||
'combinedFamilyInstall',
|
||||
'commandBodyConverter',
|
||||
'doneBannerStyle',
|
||||
'flatCommandDir',
|
||||
'frontmatterDialect',
|
||||
'globalDirResolver',
|
||||
'hookPathStyle',
|
||||
'hooksJsonSurface',
|
||||
'hyphenNameAgentBody',
|
||||
'installsCommandBodiesForWorkflowDelegation',
|
||||
'legacyCommandsGsdCleanup',
|
||||
'legacyCommandsGsdInstallMigration',
|
||||
'legacyCommandsGsdUninstall',
|
||||
'legacyDevinSkillsCleanup',
|
||||
'localCommandsViaRules',
|
||||
'localInstallDeferred',
|
||||
'localInstallStyle',
|
||||
'localTargetIsProjectRoot',
|
||||
'managedHookEvents',
|
||||
'mcpCompanion',
|
||||
'namedSubagentsSupported',
|
||||
'nativeModelAliases',
|
||||
'nativePlugin',
|
||||
'noPathRewrite',
|
||||
'ownsClaudePaths',
|
||||
'permissionsSchema',
|
||||
'pluginOnlyInstall',
|
||||
'projectInstructionFile',
|
||||
'reapplyCommand',
|
||||
'reportCommandsDir',
|
||||
'reportSkillsCount',
|
||||
'retiredArtifacts',
|
||||
'settingsFileByScope',
|
||||
'sharedHooksDirName',
|
||||
'skillFrontmatterVersion',
|
||||
'skillPriorityFrontmatter',
|
||||
'skillsGlobalOnboarding',
|
||||
'skillsManifestPrefix',
|
||||
'skipCodexSkillsManifest',
|
||||
'skipHomePrefixSubstitution',
|
||||
'skipSettingsUi',
|
||||
'skipSharedHooksInstall',
|
||||
'skipUpdateBannerCommand',
|
||||
'soloStageMetadata',
|
||||
'sourceMarkerFile',
|
||||
'tomlConfigInstall',
|
||||
'trackCategoryDescription',
|
||||
'verificationStyle',
|
||||
'writeCategoryDescription',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Frozen reason codes for the non-fatal reviewer diagnostics (ADR-2782 D4.3).
|
||||
*
|
||||
* The IR behind `collectReviewerWarnings`' rendered strings. Tests assert on
|
||||
* these codes; the rendered `message` is operator console output and tests must
|
||||
* not depend on it (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
|
||||
* Outputs"), which is the same split `bin/verify-reapply-patches.cjs` uses.
|
||||
*
|
||||
* Adding a code is THREE coordinated changes: this enum, the emitting site in
|
||||
* `collectReviewerWarningRecordFields`, and the test that locks
|
||||
* `Object.keys(REVIEWER_WARNING).sort()`. That coupling is the point — it stops
|
||||
* the code surface drifting from the test surface.
|
||||
*/
|
||||
const REVIEWER_WARNING = Object.freeze({
|
||||
/** A key inside a `reviewer` body that this GSD version does not know. */
|
||||
UNKNOWN_REVIEWER_FIELD: 'unknown_reviewer_field',
|
||||
/** A `runtime.hostBehaviors` key that was removed from the vocabulary. */
|
||||
REMOVED_HOST_BEHAVIOR: 'removed_host_behavior',
|
||||
/** A `runtime.hostBehaviors` key outside the closed vocabulary. */
|
||||
UNKNOWN_HOST_BEHAVIOR: 'unknown_host_behavior',
|
||||
});
|
||||
|
||||
/** Dotted path of the field removed by ADR-2782 D9 / #2801. */
|
||||
const REMOVED_REVIEWER_CLI_FIELD = 'runtime.hostBehaviors.reviewerCli';
|
||||
|
||||
const KNOWN_PROBE_FIELDS = new Set(['kind', 'binary', 'needle', 'timeoutMs', 'hostConfigKey', 'path']);
|
||||
|
||||
/** A bounded probe timeout must be a finite, positive INTEGER of milliseconds. */
|
||||
@@ -1615,6 +1731,41 @@ function describeValue(v) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ceiling on how many undeclared-key diagnostics one capability may produce.
|
||||
*
|
||||
* The loops below iterate MANIFEST-SUPPLIED keys, and an installed third-party
|
||||
* manifest is bounded only by MANIFEST_MAX_BYTES (8MB). Unbounded, one manifest
|
||||
* of 800k keys yields 800k records and ~139MB of message text, retained for the
|
||||
* registry's lifetime in OverlayMeta.diagnostics. Ten is enough to act on; the
|
||||
* rest are summarized. Mirrors the existing truncation idiom in
|
||||
* `capability-loader.cts` (`crossErrs.slice(0, 3)`).
|
||||
*/
|
||||
const MAX_REPORTED_UNKNOWN_KEYS = 10;
|
||||
|
||||
/** Ceiling on how much of one manifest-supplied key name a diagnostic repeats. */
|
||||
const MAX_REPORTED_KEY_CHARS = 80;
|
||||
|
||||
/**
|
||||
* Render a manifest-supplied KEY for a diagnostic: control-safe and bounded.
|
||||
*
|
||||
* Key names carry no grammar anywhere — unlike `cap.id`, which `validateCapability`
|
||||
* gates on KEBAB_RE before these diagnostics run — so a key is fully
|
||||
* attacker-controlled text heading for stderr and OverlayMeta.warnings. C0/C1
|
||||
* controls (ESC, CR, LF) become U+FFFD so a key cannot emit terminal escapes or
|
||||
* forge a log line, and the result is clipped so one key cannot carry megabytes
|
||||
* into a retained diagnostic.
|
||||
*
|
||||
* @param {*} key
|
||||
* @returns {string}
|
||||
*/
|
||||
function describeKey(key) {
|
||||
const raw = typeof key === 'string' ? key : String(key);
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const safe = raw.replace(/[\x00-\x1f\x7f-\x9f]/g, '<27>');
|
||||
return safe.length > MAX_REPORTED_KEY_CHARS ? safe.slice(0, MAX_REPORTED_KEY_CHARS) + '…' : safe;
|
||||
}
|
||||
|
||||
/** Extract a message from an unknown thrown value without throwing again. */
|
||||
function safeErrorMessage(err) {
|
||||
try {
|
||||
@@ -1661,7 +1812,39 @@ function validateEnumField(ctx, label, value, validSet) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect NON-FATAL diagnostics for a reviewer body (ADR-2782 D4.3).
|
||||
* Collect NON-FATAL reviewer diagnostics for a capability manifest as TYPED
|
||||
* RECORDS (ADR-2782 D4.3, plus the D9 `hostBehaviors.reviewerCli` removal
|
||||
* notice, #2801).
|
||||
*
|
||||
* This is the IR. `collectReviewerWarnings` below renders it to strings for the
|
||||
* two production consumers; tests assert on these records instead of matching
|
||||
* the rendered prose (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
|
||||
* Outputs").
|
||||
*
|
||||
* Record shape — `code` and `capId` are always present; the rest is per-code:
|
||||
* { code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
||||
* capId, field: 'reviewer.<key>', knownFields: string[], message }
|
||||
* { code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
|
||||
* capId, field: 'runtime.hostBehaviors.reviewerCli',
|
||||
* replacement: 'reviewer', docs: '<how-to path>', message }
|
||||
*
|
||||
* TOTAL: returns an array for ANY input and never throws. It runs on
|
||||
* loadRegistry's ACCEPT path, so a diagnostic that throws would cost the user a
|
||||
* lane that is otherwise perfectly valid (#1461 OVL-1).
|
||||
*
|
||||
* @param {object} cap A capability manifest.
|
||||
* @returns {Array<object>} Warning records; empty when there is nothing to say.
|
||||
*/
|
||||
function collectReviewerWarningRecords(cap) {
|
||||
try {
|
||||
return collectReviewerWarningRecordFields(cap);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the IR for operator console output.
|
||||
*
|
||||
* Kept separate from validateReviewerBody so validateCapability's contract
|
||||
* (`=> string[]` of ERRORS) is unchanged for its two existing callers. The
|
||||
@@ -1669,35 +1852,135 @@ function validateEnumField(ctx, label, value, validSet) {
|
||||
* through OverlayMeta.warnings. A warning written only to a build log nobody
|
||||
* reads is not a warning (ADR-2782 D4, "Where warnings surface").
|
||||
*
|
||||
* The `string[]` shape is load-bearing for both consumers and does not change.
|
||||
*
|
||||
* @param {object} cap A capability manifest.
|
||||
* @returns {string[]} Warning strings; empty when there is nothing to say.
|
||||
*/
|
||||
function collectReviewerWarnings(cap) {
|
||||
// Same totality contract as validateReviewerBody, for the same reason: this is
|
||||
// called from loadRegistry's accept path, and a diagnostic that throws must
|
||||
// never cost the user a working lane.
|
||||
try {
|
||||
return collectReviewerWarningFields(cap);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return collectReviewerWarningRecords(cap).map((record) => record.message);
|
||||
}
|
||||
|
||||
function collectReviewerWarningFields(cap) {
|
||||
const warnings = [];
|
||||
if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return warnings;
|
||||
const r = cap.reviewer;
|
||||
if (typeof r !== 'object' || r === null || Array.isArray(r)) return warnings;
|
||||
function collectReviewerWarningRecordFields(cap) {
|
||||
const records = [];
|
||||
if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return records;
|
||||
|
||||
const capId = typeof cap.id === 'string' ? cap.id : '(unknown)';
|
||||
|
||||
// ADR-2782 D9 / #2801 — the REMOVED `runtime.hostBehaviors.reviewerCli` alias.
|
||||
//
|
||||
// Emitted BEFORE the `reviewer`-body early-return below, deliberately. The
|
||||
// manifest this notice exists for is the ALIAS-ONLY one, which by definition
|
||||
// carries no body; after that guard the check would fire only for
|
||||
// capabilities that already declare a lane — exactly the set that does not
|
||||
// need telling.
|
||||
//
|
||||
// Presence-based, not `=== true`: the key is unknown at ANY value now, which
|
||||
// is the same rule the unknown-`reviewer.*`-field loop below applies. Own-key
|
||||
// read, so a polluted prototype cannot manufacture this warning on every
|
||||
// otherwise-innocent manifest. This is ONE keyed removal notice, not general
|
||||
// `hostBehaviors` validation — the closed vocabulary below handles that (#2801).
|
||||
const runtimeBody = cap.runtime;
|
||||
if (typeof runtimeBody === 'object' && runtimeBody !== null && !Array.isArray(runtimeBody)) {
|
||||
const hostBehaviors = runtimeBody.hostBehaviors;
|
||||
if (
|
||||
typeof hostBehaviors === 'object'
|
||||
&& hostBehaviors !== null
|
||||
&& !Array.isArray(hostBehaviors)
|
||||
&& Object.prototype.hasOwnProperty.call(hostBehaviors, 'reviewerCli')
|
||||
) {
|
||||
records.push({
|
||||
code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
|
||||
capId,
|
||||
field: REMOVED_REVIEWER_CLI_FIELD,
|
||||
replacement: 'reviewer',
|
||||
docs: 'docs/how-to/ship-a-reviewer-lane.md',
|
||||
message:
|
||||
'⚠ capability "' + capId + '" ' + REMOVED_REVIEWER_CLI_FIELD + ' was removed (ADR-2782 D9) ' +
|
||||
'— ignored, and it contributes no reviewer lane. Declare a `reviewer` body instead; see ' +
|
||||
'docs/how-to/ship-a-reviewer-lane.md',
|
||||
});
|
||||
}
|
||||
|
||||
// #2801 — the closed `hostBehaviors` vocabulary (ADR-1016). `reviewerCli` is
|
||||
// excluded here: it already drew its own removal notice above, and a second,
|
||||
// generic "unknown key" record for the same key would be noise, not signal.
|
||||
if (typeof hostBehaviors === 'object' && hostBehaviors !== null && !Array.isArray(hostBehaviors)) {
|
||||
let reported = 0;
|
||||
let omitted = 0;
|
||||
for (const key of Object.keys(hostBehaviors)) {
|
||||
if (isReservedName(key) || key === 'reviewerCli' || KNOWN_HOST_BEHAVIORS.has(key)) continue;
|
||||
if (reported >= MAX_REPORTED_UNKNOWN_KEYS) {
|
||||
omitted += 1;
|
||||
continue;
|
||||
}
|
||||
reported += 1;
|
||||
const safeKey = describeKey(key);
|
||||
records.push({
|
||||
code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
|
||||
capId,
|
||||
field: 'runtime.hostBehaviors.' + safeKey,
|
||||
message:
|
||||
'⚠ capability "' + capId + '" runtime.hostBehaviors.' + safeKey + ' is not a known host behavior ' +
|
||||
'in this GSD version — ignored. Adding one is a reviewed first-party change (ADR-1016).',
|
||||
});
|
||||
}
|
||||
if (omitted > 0) {
|
||||
records.push({
|
||||
code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
|
||||
capId,
|
||||
field: 'runtime.hostBehaviors',
|
||||
truncated: true,
|
||||
omittedCount: omitted,
|
||||
message:
|
||||
'⚠ capability "' + capId + '" declares ' + omitted + ' further unknown runtime.hostBehaviors ' +
|
||||
'key(s), not listed. A manifest this far outside the vocabulary is likely built for a ' +
|
||||
'different GSD version.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const r = cap.reviewer;
|
||||
if (typeof r !== 'object' || r === null || Array.isArray(r)) return records;
|
||||
|
||||
// Same ceiling and the same key sanitization as the hostBehaviors sweep above.
|
||||
// This loop predates #2801 and carried both defects; fixing only the new copy
|
||||
// would leave the identical defect one screen away from its own fix.
|
||||
let reportedFields = 0;
|
||||
let omittedFields = 0;
|
||||
for (const key of Object.keys(r)) {
|
||||
if (isReservedName(key) || KNOWN_REVIEWER_FIELDS.has(key)) continue;
|
||||
warnings.push(
|
||||
'⚠ capability "' + capId + '" reviewer.' + key + ' is not a known reviewer field ' +
|
||||
'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
|
||||
);
|
||||
if (reportedFields >= MAX_REPORTED_UNKNOWN_KEYS) {
|
||||
omittedFields += 1;
|
||||
continue;
|
||||
}
|
||||
reportedFields += 1;
|
||||
const safeKey = describeKey(key);
|
||||
records.push({
|
||||
code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
||||
capId,
|
||||
field: 'reviewer.' + safeKey,
|
||||
knownFields: [...KNOWN_REVIEWER_FIELDS],
|
||||
message:
|
||||
'⚠ capability "' + capId + '" reviewer.' + safeKey + ' is not a known reviewer field ' +
|
||||
'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
|
||||
});
|
||||
}
|
||||
return warnings;
|
||||
if (omittedFields > 0) {
|
||||
records.push({
|
||||
code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
||||
capId,
|
||||
field: 'reviewer',
|
||||
knownFields: [...KNOWN_REVIEWER_FIELDS],
|
||||
truncated: true,
|
||||
omittedCount: omittedFields,
|
||||
message:
|
||||
'⚠ capability "' + capId + '" declares ' + omittedFields + ' further unknown reviewer field(s), ' +
|
||||
'not listed.',
|
||||
});
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3170,8 +3453,14 @@ module.exports = {
|
||||
VALID_EVIDENCE_CLASSES,
|
||||
VALID_LANE_HANDLERS,
|
||||
KNOWN_REVIEWER_FIELDS,
|
||||
KNOWN_HOST_BEHAVIORS,
|
||||
MAX_REPORTED_UNKNOWN_KEYS,
|
||||
MAX_REPORTED_KEY_CHARS,
|
||||
validateReviewerBody,
|
||||
collectReviewerWarnings,
|
||||
collectReviewerWarningRecords,
|
||||
REVIEWER_WARNING,
|
||||
REMOVED_REVIEWER_CLI_FIELD,
|
||||
VALID_INSTALL_SURFACES,
|
||||
VALID_PERMISSION_WRITERS,
|
||||
VALID_EXTENDED_HOOK_EVENTS,
|
||||
|
||||
Reference in New Issue
Block a user