@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +30 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +1568 -622
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +761 -84
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +45 -4
- package/gsd-core/bin/lib/init.cjs +76 -39
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +228 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +40 -6
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +14 -9
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +37 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +12 -9
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +6 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +6 -0
- package/package.json +9 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +8 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/run-tests.cjs +21 -1
- package/scripts/validate-registry.cjs +117 -0
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -688,7 +688,7 @@ async function main() {
|
|
|
688
688
|
// phase / roadmap / milestone / progress / etc.
|
|
689
689
|
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
690
690
|
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
|
|
691
|
-
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
|
|
691
|
+
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
692
692
|
'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
693
693
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
694
694
|
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
@@ -1291,6 +1291,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1291
1291
|
break;
|
|
1292
1292
|
}
|
|
1293
1293
|
|
|
1294
|
+
case 'normalize-test-command': {
|
|
1295
|
+
// #1857: rewrite a resolved test command to a one-shot form so a
|
|
1296
|
+
// watch-mode runner (vitest/jest) cannot hang a verification gate. Shared
|
|
1297
|
+
// by the regression gate and the post-merge gate. args[1] is the raw
|
|
1298
|
+
// resolved command; --cwd (already parsed into `cwd`) locates package.json.
|
|
1299
|
+
const testCommandNormalizer = require('./lib/normalize-test-command.cjs');
|
|
1300
|
+
testCommandNormalizer.cmdNormalizeTestCommand(cwd, args[1]);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1294
1304
|
case 'dispatch-should-flatten': {
|
|
1295
1305
|
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
|
1296
1306
|
//
|
|
@@ -2140,6 +2150,28 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
2140
2150
|
}
|
|
2141
2151
|
}
|
|
2142
2152
|
} catch { /* best-effort — list still works without the inactive annotation */ }
|
|
2153
|
+
// Issue #2045 (DEFECT 3): derive each capability's SURFACED state from the
|
|
2154
|
+
// SAME resolver `capability state` uses (resolveCapabilityRuntimeState), so
|
|
2155
|
+
// `list` and `state` stop disagreeing. `list` previously derived `status`
|
|
2156
|
+
// purely from ledger-entry existence — an installed-but-not-surfaced cap
|
|
2157
|
+
// reported active in `list` and absent in `state`. Surfaced is evaluated at
|
|
2158
|
+
// the default runtime config dir (the resolver resolves it when undefined),
|
|
2159
|
+
// matching `capability state <id>` with no --config-dir. Best-effort: a
|
|
2160
|
+
// resolver failure leaves surfacedById empty (rows report surfaced:null).
|
|
2161
|
+
const surfacedById = {};
|
|
2162
|
+
// surfacedById is keyed by capId only (NOT `${scope} ${capId}`): surface
|
|
2163
|
+
// state is single-source — one runtime config dir → one .gsd-surface.json
|
|
2164
|
+
// → one surfaced truth per capId — and the loader dedupes overlay caps to
|
|
2165
|
+
// one registry entry per id (first-party-wins). So a cap installed in both
|
|
2166
|
+
// scopes correctly shares one surfaced value across its list rows.
|
|
2167
|
+
try {
|
|
2168
|
+
const surfaceState = capabilityState.resolveCapabilityRuntimeState(cwd, undefined);
|
|
2169
|
+
for (const cap of (surfaceState && surfaceState.capabilities) || []) {
|
|
2170
|
+
if (cap && typeof cap.id === 'string') {
|
|
2171
|
+
surfacedById[cap.id] = cap.surfaced === true;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
} catch { /* best-effort — list still works without the surfaced annotation */ }
|
|
2143
2175
|
for (const capId of Object.keys(fp)) {
|
|
2144
2176
|
const cap = fp[capId] || {};
|
|
2145
2177
|
rows.push({
|
|
@@ -2150,6 +2182,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
2150
2182
|
source: 'first-party',
|
|
2151
2183
|
scope: 'first-party',
|
|
2152
2184
|
status: 'active',
|
|
2185
|
+
surfaced: Object.prototype.hasOwnProperty.call(surfacedById, capId) ? surfacedById[capId] === true : null,
|
|
2153
2186
|
title: cap.title || null,
|
|
2154
2187
|
});
|
|
2155
2188
|
}
|
|
@@ -2199,6 +2232,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
2199
2232
|
scope: sc,
|
|
2200
2233
|
status,
|
|
2201
2234
|
reason,
|
|
2235
|
+
// Issue #2045 (DEFECT 3): surfaced reflects surface composition, so
|
|
2236
|
+
// list and state agree. An inactive (unconsented/incompatible) cap is
|
|
2237
|
+
// surfaced:false by definition; otherwise defer to the resolver.
|
|
2238
|
+
surfaced: status === 'active'
|
|
2239
|
+
? (Object.prototype.hasOwnProperty.call(surfacedById, capId) ? surfacedById[capId] === true : null)
|
|
2240
|
+
: false,
|
|
2202
2241
|
title: manifest.title || null,
|
|
2203
2242
|
});
|
|
2204
2243
|
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API-Coverage detector + matrix validator (#1562).
|
|
4
|
+
*
|
|
5
|
+
* The enforcement half of "Full API Coverage by Default — Opt Out, Never Opt In."
|
|
6
|
+
* When a phase integrates an external API/service/SDK, the planner must produce a
|
|
7
|
+
* coverage matrix (COVERAGE.md) enumerating the API's capability surface; every
|
|
8
|
+
* non-integrated capability is an explicit, reasoned opt-out. The seal-time gate
|
|
9
|
+
* (capabilities/ai-integration, verify:pre) consumes this module to (a) detect
|
|
10
|
+
* whether a phase integrates an external API and (b) validate the produced matrix.
|
|
11
|
+
*
|
|
12
|
+
* Design notes (rubber-duck'd):
|
|
13
|
+
* - DETERMINISTIC + TYPED IR. Both the "does this phase integrate an external
|
|
14
|
+
* API?" decision and the "is this matrix complete?" decision are pure
|
|
15
|
+
* functions returning typed IR, not LLM judgments — so the low-false-positive
|
|
16
|
+
* guarantee (acceptance criterion #4) and the completeness guarantee
|
|
17
|
+
* (acceptance #2) are testable. Mirrors assumption-delta.cts (#1561).
|
|
18
|
+
* - COMPOUND SIGNAL for low false positives. A bare word like "api" appears in
|
|
19
|
+
* countless non-integration phases ("the public API of UserController"). The
|
|
20
|
+
* detector requires an INTEGRATION VERB co-occurring with an EXTERNAL-API
|
|
21
|
+
* NOUN (or an explicit "<Service> API/SDK" phrase). Single weak tokens do not
|
|
22
|
+
* fire. This is the issue's "low false-positive trigger" made mechanical.
|
|
23
|
+
* - FENCED CODE BLOCKS ARE STRIPPED first (markdown-sectionizer seam) so a
|
|
24
|
+
* trigger term inside a code snippet does not fire.
|
|
25
|
+
* - THE DETECTOR IS A FALLBACK. The primary path is the plan:pre contribution
|
|
26
|
+
* prompting COVERAGE.md creation. The detector runs only when COVERAGE.md is
|
|
27
|
+
* ABSENT, to catch the "nobody decided" case (acceptance #1). Its precision
|
|
28
|
+
* therefore matters but is not the only line of defense.
|
|
29
|
+
* - MATRIX FORMAT. The matrix is a markdown table (human-editable, diff-friendly)
|
|
30
|
+
* with a header row `| capability | decision | reason |` and one row per
|
|
31
|
+
* capability. decision ∈ {INTEGRATE, OPT-OUT}. An OPT-OUT row MUST carry a
|
|
32
|
+
* non-empty reason. A fenced ```coverage JSON block is also accepted for
|
|
33
|
+
* machine-generated matrices. This dual shape is bijective (parse/render
|
|
34
|
+
* round-trip) and covered by a fast-check property test.
|
|
35
|
+
* - ADDITIVE-ONLY VOCABULARY (Hyrum's Law). Once shipped, the verb/noun sets
|
|
36
|
+
* are depended-upon interfaces; they only grow. Tunable via the `terms`
|
|
37
|
+
* parameter so teams can widen them without forking.
|
|
38
|
+
*
|
|
39
|
+
* Public API:
|
|
40
|
+
* detectApiIntegration(text, terms?) -> { detected, signals, terms }
|
|
41
|
+
* parseCoverageMatrix(text) -> { rows, errors, format }
|
|
42
|
+
* validateCoverageMatrix(text) -> { valid, errors, counts }
|
|
43
|
+
* renderCoverageMatrix(rows) -> string
|
|
44
|
+
* DEFAULT_API_COVERAGE_TERMS
|
|
45
|
+
*
|
|
46
|
+
* CLI:
|
|
47
|
+
* echo "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs [--json]
|
|
48
|
+
* exit 0 = integration detected, 1 = none, 2 = startup error
|
|
49
|
+
*/
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.DEFAULT_API_COVERAGE_TERMS = void 0;
|
|
52
|
+
exports.detectApiIntegration = detectApiIntegration;
|
|
53
|
+
exports.parseCoverageMatrix = parseCoverageMatrix;
|
|
54
|
+
exports.validateCoverageMatrix = validateCoverageMatrix;
|
|
55
|
+
exports.renderCoverageMatrix = renderCoverageMatrix;
|
|
56
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
57
|
+
/**
|
|
58
|
+
* Curated default trigger vocabulary. ADDITIVE-ONLY (Hyrum's Law). Tunable via
|
|
59
|
+
* the `terms` parameter.
|
|
60
|
+
*
|
|
61
|
+
* VERBS are deliberately conservative: common verbs like "add", "use", "call",
|
|
62
|
+
* "implement" are EXCLUDED because they appear in nearly every phase and would
|
|
63
|
+
* make the gate fire on prose that has nothing to do with an external API. The
|
|
64
|
+
* verbs kept all connote BRINGING IN an external surface.
|
|
65
|
+
*
|
|
66
|
+
* NOUNS name an external-API surface. Bare "client" is excluded — too ambiguous
|
|
67
|
+
* (client-side UI vs API client). "service" alone is excluded (internal
|
|
68
|
+
* services); a phase integrating an external service virtually always pairs it
|
|
69
|
+
* with "API"/"SDK"/"REST"/etc., which the compound verb+noun rule captures.
|
|
70
|
+
*/
|
|
71
|
+
exports.DEFAULT_API_COVERAGE_TERMS = {
|
|
72
|
+
verbs: [
|
|
73
|
+
'integrate',
|
|
74
|
+
'integrates',
|
|
75
|
+
'integrating',
|
|
76
|
+
'integration',
|
|
77
|
+
'wrap',
|
|
78
|
+
'wraps',
|
|
79
|
+
'wrapping',
|
|
80
|
+
'connect',
|
|
81
|
+
'connects',
|
|
82
|
+
'connecting',
|
|
83
|
+
'consume',
|
|
84
|
+
'consumes',
|
|
85
|
+
'consuming',
|
|
86
|
+
'wire',
|
|
87
|
+
'wires',
|
|
88
|
+
'wiring',
|
|
89
|
+
'onboard',
|
|
90
|
+
'onboarding',
|
|
91
|
+
'adopt',
|
|
92
|
+
'adopts',
|
|
93
|
+
'adopting',
|
|
94
|
+
],
|
|
95
|
+
nouns: [
|
|
96
|
+
'api',
|
|
97
|
+
'apis',
|
|
98
|
+
'sdk',
|
|
99
|
+
'sdks',
|
|
100
|
+
'rest',
|
|
101
|
+
'graphql',
|
|
102
|
+
'grpc',
|
|
103
|
+
'endpoint',
|
|
104
|
+
'endpoints',
|
|
105
|
+
'oauth',
|
|
106
|
+
'oauth2',
|
|
107
|
+
'webhook',
|
|
108
|
+
'webhooks',
|
|
109
|
+
'mcp',
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
/** Hardening caps for the tunable vocabulary (hostile `--terms` defense). */
|
|
113
|
+
const MAX_TERMS_PER_KIND = 200;
|
|
114
|
+
const MAX_TERM_LEN = 32;
|
|
115
|
+
/**
|
|
116
|
+
* Field-length caps for matrix cell values. Cell content flows from a
|
|
117
|
+
* semi-trusted COVERAGE.md into the gate `message` that the orchestrator LLM
|
|
118
|
+
* reads, so it is bounded to keep the prompt-injection surface small and to
|
|
119
|
+
* document the format contract (short, single-line prose — not paragraphs).
|
|
120
|
+
*/
|
|
121
|
+
const CAPABILITY_MAX_LEN = 80;
|
|
122
|
+
const REASON_MAX_LEN = 200;
|
|
123
|
+
function normalizeTerms(list) {
|
|
124
|
+
if (!Array.isArray(list))
|
|
125
|
+
return [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const raw of list) {
|
|
129
|
+
if (typeof raw !== 'string')
|
|
130
|
+
continue;
|
|
131
|
+
const t = raw.trim().toLowerCase().slice(0, MAX_TERM_LEN);
|
|
132
|
+
if (!t || !/[a-z0-9]/.test(t))
|
|
133
|
+
continue;
|
|
134
|
+
if (seen.has(t))
|
|
135
|
+
continue;
|
|
136
|
+
seen.add(t);
|
|
137
|
+
out.push(t);
|
|
138
|
+
if (out.length >= MAX_TERMS_PER_KIND)
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function resolveTerms(terms) {
|
|
144
|
+
const merge = (key) => {
|
|
145
|
+
const t = terms && terms[key];
|
|
146
|
+
return Array.isArray(t) ? normalizeTerms(t) : [...exports.DEFAULT_API_COVERAGE_TERMS[key]];
|
|
147
|
+
};
|
|
148
|
+
return { verbs: merge('verbs'), nouns: merge('nouns') };
|
|
149
|
+
}
|
|
150
|
+
function escapeRegex(s) {
|
|
151
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
152
|
+
}
|
|
153
|
+
function makeSnippet(line, anchor) {
|
|
154
|
+
const cleaned = line.replace(/\s+/g, ' ').trim();
|
|
155
|
+
if (cleaned.length <= 120)
|
|
156
|
+
return cleaned;
|
|
157
|
+
const idx = cleaned.toLowerCase().indexOf(anchor);
|
|
158
|
+
if (idx < 0)
|
|
159
|
+
return cleaned.slice(0, 120);
|
|
160
|
+
const start = Math.max(0, idx - 50);
|
|
161
|
+
const end = Math.min(cleaned.length, idx + anchor.length + 50);
|
|
162
|
+
const prefix = start > 0 ? '…' : '';
|
|
163
|
+
const suffix = end < cleaned.length ? '…' : '';
|
|
164
|
+
return `${prefix}${cleaned.slice(start, end)}${suffix}`;
|
|
165
|
+
}
|
|
166
|
+
/** `<Service> API` / `<Service> SDK` — a capitalized proper noun immediately
|
|
167
|
+
* followed by API/SDK. Strong signal on its own (no verb required).
|
|
168
|
+
*
|
|
169
|
+
* STOPWORDS guard against the false positive where an ordinary capitalized
|
|
170
|
+
* sentence starter ("The API …", "An SDK …", "Our REST …") matches the
|
|
171
|
+
* `[A-Z]\w+ API` shape. Those are common English, not a service name, so they
|
|
172
|
+
* are rejected before counting as a surface signal (acceptance #4 — low false
|
|
173
|
+
* positives). */
|
|
174
|
+
const SERVICE_SURFACE_API_RE = /\b([A-Z][A-Za-z0-9_-]{1,})\s+(API|SDK|REST|GraphQL)\b/;
|
|
175
|
+
const SERVICE_STOPWORDS = new Set([
|
|
176
|
+
'the', 'an', 'a', 'our', 'this', 'these', 'that', 'those', 'new', 'add',
|
|
177
|
+
'use', 'your', 'my', 'no', 'some', 'any', 'all', 'each', 'every', 'both',
|
|
178
|
+
'if', 'when', 'while', 'with', 'via', 'using', 'into', 'its', 'their',
|
|
179
|
+
'we', 'you', 'they', 'it',
|
|
180
|
+
]);
|
|
181
|
+
/**
|
|
182
|
+
* Detect whether phase-scope prose describes integrating an external API/SDK.
|
|
183
|
+
*
|
|
184
|
+
* Fires when EITHER:
|
|
185
|
+
* (a) a compound verb+noun signal co-occurs on the same line, OR
|
|
186
|
+
* (b) an explicit `<Service> API|SDK|REST|GraphQL` surface appears.
|
|
187
|
+
*
|
|
188
|
+
* Non-string inputs degrade to `{ detected: false }` without throwing.
|
|
189
|
+
*/
|
|
190
|
+
function detectApiIntegration(text, terms) {
|
|
191
|
+
const effective = resolveTerms(terms);
|
|
192
|
+
if (typeof text !== 'string') {
|
|
193
|
+
return { detected: false, signals: [], terms: effective };
|
|
194
|
+
}
|
|
195
|
+
const stripped = (0, markdown_sectionizer_cjs_1.stripFencedCode)(text.replace(/\r\n/g, '\n')).text;
|
|
196
|
+
if (stripped.trim().length === 0) {
|
|
197
|
+
return { detected: false, signals: [], terms: effective };
|
|
198
|
+
}
|
|
199
|
+
const signals = [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
const lines = stripped.split('\n');
|
|
202
|
+
// (a) compound verb+noun on the same line.
|
|
203
|
+
if (effective.verbs.length > 0 && effective.nouns.length > 0) {
|
|
204
|
+
const verbRe = new RegExp('(^|[^a-zA-Z0-9])(' + effective.verbs.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)', 'gi');
|
|
205
|
+
const nounRe = new RegExp('(^|[^a-zA-Z0-9])(' + effective.nouns.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)', 'gi');
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
verbRe.lastIndex = 0;
|
|
208
|
+
nounRe.lastIndex = 0;
|
|
209
|
+
const vMatch = verbRe.exec(line);
|
|
210
|
+
if (!vMatch)
|
|
211
|
+
continue;
|
|
212
|
+
const nMatch = nounRe.exec(line);
|
|
213
|
+
if (!nMatch)
|
|
214
|
+
continue;
|
|
215
|
+
const verb = (vMatch[2] || '').toLowerCase();
|
|
216
|
+
const noun = (nMatch[2] || '').toLowerCase();
|
|
217
|
+
const key = `${verb}+${noun}`;
|
|
218
|
+
if (seen.has(key))
|
|
219
|
+
continue;
|
|
220
|
+
seen.add(key);
|
|
221
|
+
signals.push({ verb, noun, snippet: makeSnippet(line, noun) });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// (b) explicit <Service> API|SDK|REST|GraphQL surface.
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
SERVICE_SURFACE_API_RE.lastIndex = 0;
|
|
227
|
+
const m = SERVICE_SURFACE_API_RE.exec(line);
|
|
228
|
+
if (!m)
|
|
229
|
+
continue;
|
|
230
|
+
// Reject ordinary capitalized sentence starters ("The API …", "Our REST …").
|
|
231
|
+
if (SERVICE_STOPWORDS.has((m[1] || '').toLowerCase()))
|
|
232
|
+
continue;
|
|
233
|
+
const noun = (m[2] || '').toLowerCase();
|
|
234
|
+
const key = `surface+${noun}`;
|
|
235
|
+
if (seen.has(key))
|
|
236
|
+
continue;
|
|
237
|
+
seen.add(key);
|
|
238
|
+
signals.push({ verb: '(surface)', noun, snippet: makeSnippet(line, m[1]) });
|
|
239
|
+
}
|
|
240
|
+
return { detected: signals.length > 0, signals, terms: effective };
|
|
241
|
+
}
|
|
242
|
+
const VALID_DECISIONS = new Set(['INTEGRATE', 'OPT-OUT']);
|
|
243
|
+
/**
|
|
244
|
+
* Parse a coverage matrix from COVERAGE.md. Accepts two bijective formats:
|
|
245
|
+
*
|
|
246
|
+
* 1. Markdown table (canonical, human-editable):
|
|
247
|
+
* | capability | decision | reason |
|
|
248
|
+
* |---|---|---|
|
|
249
|
+
* | search | INTEGRATE | |
|
|
250
|
+
* | playlists | OPT-OUT | not needed yet |
|
|
251
|
+
*
|
|
252
|
+
* 2. Fenced ```coverage JSON block (machine-generated):
|
|
253
|
+
* ```coverage
|
|
254
|
+
* [ {"capability":"search","decision":"INTEGRATE","reason":""}, ... ]
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* Rows are trimmed; decisions upper-cased; missing reason → "". Returns
|
|
258
|
+
* `{ rows: [], errors: [], format: 'none' }` for empty/non-matrix input.
|
|
259
|
+
*/
|
|
260
|
+
function parseCoverageMatrix(text) {
|
|
261
|
+
const out = { rows: [], errors: [], format: 'none' };
|
|
262
|
+
if (typeof text !== 'string')
|
|
263
|
+
return out;
|
|
264
|
+
const src = text.replace(/\r\n/g, '\n');
|
|
265
|
+
// (1) fenced ```coverage JSON block takes precedence if present.
|
|
266
|
+
// Case-insensitive info string (```coverage and ```Coverage are both legal CommonMark).
|
|
267
|
+
// allow-adhoc-markdown: extracting a NAMED ```coverage fence (extraction of one tagged block), not stripping all fences — stripFencedCode/extractTaggedBlocks do not cover named-fence extraction.
|
|
268
|
+
const fenceMatch = src.match(/```coverage\s*\n([\s\S]*?)\n```/i);
|
|
269
|
+
if (fenceMatch && fenceMatch[1]) {
|
|
270
|
+
out.format = 'json';
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(fenceMatch[1]);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
out.errors.push('fenced ```coverage block is not valid JSON');
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
if (!Array.isArray(parsed)) {
|
|
280
|
+
out.errors.push('fenced ```coverage block must be a JSON array');
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
284
|
+
const row = rowFromJson(parsed[i]);
|
|
285
|
+
if ('error' in row) {
|
|
286
|
+
out.errors.push(`row[${i}]: ${row.error}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out.rows.push(row);
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
// (2) markdown table — collect table rows whose decision column parses.
|
|
294
|
+
const lines = src.split('\n');
|
|
295
|
+
let sawHeader = false;
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const trimmed = line.trim();
|
|
298
|
+
if (!trimmed.startsWith('|'))
|
|
299
|
+
continue;
|
|
300
|
+
const cells = trimmed.slice(1, trimmed.endsWith('|') ? -1 : trimmed.length).split('|');
|
|
301
|
+
if (cells.length < 2)
|
|
302
|
+
continue;
|
|
303
|
+
const cleaned = cells.map((c) => c.trim());
|
|
304
|
+
// skip separator rows (|---|---|); require ≥3 dashes so a literal "-" cell
|
|
305
|
+
// is not mistaken for a separator.
|
|
306
|
+
if (cleaned.every((c) => /^:?-{3,}:?$/.test(c)))
|
|
307
|
+
continue;
|
|
308
|
+
const decisionCell = (cleaned[1] || '').toUpperCase();
|
|
309
|
+
// header detection
|
|
310
|
+
if (!sawHeader && cleaned[0].toLowerCase() === 'capability') {
|
|
311
|
+
sawHeader = true;
|
|
312
|
+
out.format = 'table';
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (!VALID_DECISIONS.has(decisionCell)) {
|
|
316
|
+
// A row that otherwise looks like data (≥3 cells, non-empty capability)
|
|
317
|
+
// but carries a malformed decision is a real error, not a row to skip
|
|
318
|
+
// silently — otherwise a single typo'd row collapses the matrix to
|
|
319
|
+
// "empty" and the user sees a confusing message.
|
|
320
|
+
if (cleaned.length >= 3 && cleaned[0]) {
|
|
321
|
+
out.errors.push(`row: decision "${decisionCell}" not in {INTEGRATE, OPT-OUT}`);
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (out.format === 'none')
|
|
326
|
+
out.format = 'table';
|
|
327
|
+
// A coverage row has exactly 3 cells. Extra cells mean an unescaped pipe in
|
|
328
|
+
// a value silently corrupted the row — surface it rather than parse garbage.
|
|
329
|
+
if (cleaned.length > 3) {
|
|
330
|
+
out.errors.push(`row: ${cleaned.length} columns (expected 3 — unescaped pipe in a cell?)`);
|
|
331
|
+
}
|
|
332
|
+
out.rows.push({
|
|
333
|
+
capability: cleaned[0] || '',
|
|
334
|
+
decision: decisionCell,
|
|
335
|
+
reason: (cleaned[2] ?? '').trim(),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
function rowFromJson(v) {
|
|
341
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
342
|
+
return { error: 'not an object' };
|
|
343
|
+
const o = v;
|
|
344
|
+
const capability = typeof o['capability'] === 'string' ? o['capability'].trim() : '';
|
|
345
|
+
if (!capability)
|
|
346
|
+
return { error: 'missing/empty "capability"' };
|
|
347
|
+
const dRaw = typeof o['decision'] === 'string' ? o['decision'].trim().toUpperCase() : '';
|
|
348
|
+
if (!VALID_DECISIONS.has(dRaw)) {
|
|
349
|
+
return { error: `decision "${dRaw}" not in {INTEGRATE, OPT-OUT}` };
|
|
350
|
+
}
|
|
351
|
+
const reason = typeof o['reason'] === 'string' ? o['reason'].trim() : '';
|
|
352
|
+
return { capability, decision: dRaw, reason };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Validate a parsed matrix. A matrix is valid when:
|
|
356
|
+
* - it is non-empty (acceptance #1: "enumerating the API surface"),
|
|
357
|
+
* - every capability name is non-empty,
|
|
358
|
+
* - every decision is INTEGRATE or OPT-OUT (enforced by parser, re-checked
|
|
359
|
+
* here for defense-in-depth),
|
|
360
|
+
* - every OPT-OUT row carries a non-empty reason (acceptance #2).
|
|
361
|
+
*
|
|
362
|
+
* Un-enumerated remainder is not representable in the format — the gate blocks
|
|
363
|
+
* when an integration is detected and NO matrix exists. This validator catches
|
|
364
|
+
* a malformed/partial matrix that does exist.
|
|
365
|
+
*/
|
|
366
|
+
function validateCoverageMatrix(text) {
|
|
367
|
+
const parsed = parseCoverageMatrix(text);
|
|
368
|
+
const errors = [...parsed.errors];
|
|
369
|
+
const rows = parsed.rows;
|
|
370
|
+
if (rows.length === 0) {
|
|
371
|
+
if (errors.length === 0)
|
|
372
|
+
errors.push('matrix is empty — no capabilities enumerated');
|
|
373
|
+
return { valid: false, errors, counts: { surface: 0, integrate: 0, optout: 0 } };
|
|
374
|
+
}
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
for (let i = 0; i < rows.length; i++) {
|
|
377
|
+
const row = rows[i];
|
|
378
|
+
if (!row.capability) {
|
|
379
|
+
errors.push(`row[${i}]: empty capability name`);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Format contract + prompt-injection bound: cell values must be short,
|
|
383
|
+
// single-line, pipe-free prose (the matrix is a markdown table whose
|
|
384
|
+
// content flows into the gate message). Pipes/newlines would corrupt the
|
|
385
|
+
// table and let a COVERAGE.md inject unbounded text into the seal message.
|
|
386
|
+
if (/[|\n\r]/.test(row.capability)) {
|
|
387
|
+
errors.push(`row[${i}]: capability contains a pipe or newline (unsupported in a table cell)`);
|
|
388
|
+
}
|
|
389
|
+
if (row.capability.length > CAPABILITY_MAX_LEN) {
|
|
390
|
+
errors.push(`row[${i}]: capability exceeds ${CAPABILITY_MAX_LEN} chars`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (row.reason && /[|\n\r]/.test(row.reason)) {
|
|
394
|
+
errors.push(`row[${i}]: reason contains a pipe or newline (unsupported in a table cell)`);
|
|
395
|
+
}
|
|
396
|
+
if (row.reason.length > REASON_MAX_LEN) {
|
|
397
|
+
errors.push(`row[${i}]: reason exceeds ${REASON_MAX_LEN} chars`);
|
|
398
|
+
}
|
|
399
|
+
const key = row.capability.toLowerCase();
|
|
400
|
+
if (key && seen.has(key))
|
|
401
|
+
errors.push(`row[${i}]: duplicate capability`);
|
|
402
|
+
if (key)
|
|
403
|
+
seen.add(key);
|
|
404
|
+
if (!VALID_DECISIONS.has(row.decision)) {
|
|
405
|
+
errors.push(`row[${i}]: decision not in {INTEGRATE, OPT-OUT}`);
|
|
406
|
+
}
|
|
407
|
+
if (row.decision === 'OPT-OUT' && !row.reason) {
|
|
408
|
+
errors.push(`row[${i}]: OPT-OUT missing reason`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const counts = {
|
|
412
|
+
surface: rows.length,
|
|
413
|
+
integrate: rows.filter((r) => r.decision === 'INTEGRATE').length,
|
|
414
|
+
optout: rows.filter((r) => r.decision === 'OPT-OUT').length,
|
|
415
|
+
};
|
|
416
|
+
return { valid: errors.length === 0, errors, counts };
|
|
417
|
+
}
|
|
418
|
+
/** Render rows back to the canonical markdown-table format (bijective with parse). */
|
|
419
|
+
function renderCoverageMatrix(rows) {
|
|
420
|
+
const body = rows
|
|
421
|
+
.map((r) => `| ${r.capability} | ${r.decision} | ${r.reason} |`)
|
|
422
|
+
.join('\n');
|
|
423
|
+
return `| capability | decision | reason |\n|---|---|---|\n${body}`;
|
|
424
|
+
}
|
|
425
|
+
// ── CLI entry point ──────────────────────────────────────────────────────────
|
|
426
|
+
// Reads phase-scope text from STDIN (not argv) to avoid OS ARG_MAX limits.
|
|
427
|
+
// Invoked by workflow bash as: echo "$SCOPE" | node .../api-coverage.cjs [--json]
|
|
428
|
+
// Exit 0 = integration detected, 1 = none, 2 = startup error. Mirrors
|
|
429
|
+
// assumption-delta.cjs / ui-safety-gate.cjs.
|
|
430
|
+
if (require.main === module) {
|
|
431
|
+
const argv = process.argv.slice(2);
|
|
432
|
+
const wantJson = argv.includes('--json');
|
|
433
|
+
let termsOverride;
|
|
434
|
+
const verbsIdx = argv.indexOf('--verbs');
|
|
435
|
+
const verbsVal = verbsIdx !== -1 ? argv[verbsIdx + 1] : undefined;
|
|
436
|
+
const nounsIdx = argv.indexOf('--nouns');
|
|
437
|
+
const nounsVal = nounsIdx !== -1 ? argv[nounsIdx + 1] : undefined;
|
|
438
|
+
// A non-empty, non-flag value is an override. An EMPTY value ("") restores
|
|
439
|
+
// the curated defaults (does NOT silently zero the vocabulary).
|
|
440
|
+
const verbsOverride = typeof verbsVal === 'string' && verbsVal.length > 0 && !verbsVal.startsWith('-');
|
|
441
|
+
const nounsOverride = typeof nounsVal === 'string' && nounsVal.length > 0 && !nounsVal.startsWith('-');
|
|
442
|
+
if (verbsOverride || nounsOverride) {
|
|
443
|
+
termsOverride = {};
|
|
444
|
+
if (verbsOverride) {
|
|
445
|
+
termsOverride.verbs = verbsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
446
|
+
}
|
|
447
|
+
if (nounsOverride) {
|
|
448
|
+
termsOverride.nouns = nounsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const chunks = [];
|
|
452
|
+
process.stdin.setEncoding('utf-8');
|
|
453
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
454
|
+
process.stdin.on('end', () => {
|
|
455
|
+
const input = chunks.join('');
|
|
456
|
+
const result = detectApiIntegration(input, termsOverride);
|
|
457
|
+
if (wantJson) {
|
|
458
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
459
|
+
}
|
|
460
|
+
process.exit(result.detected ? 0 : 1);
|
|
461
|
+
});
|
|
462
|
+
process.stdin.on('error', (err) => {
|
|
463
|
+
process.stderr.write(`ERROR: api-coverage.cjs stdin read failed: ${err.message}\n`);
|
|
464
|
+
process.exit(2);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
@@ -23,6 +23,9 @@ const { planningDir } = planningWorkspace;
|
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const frontmatter = require("./frontmatter.cjs");
|
|
25
25
|
const { extractFrontmatter } = frontmatter;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
28
|
+
const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
26
29
|
const security_cjs_1 = require("./security.cjs");
|
|
27
30
|
// Terminal UAT states: `complete` (legacy) and `resolved` (post-gap-closure
|
|
28
31
|
// per workflows/execute-phase.md). Hoisted outside scanUatGaps so the Set is
|
|
@@ -357,7 +360,7 @@ function scanUatGaps(planDir) {
|
|
|
357
360
|
const results = [];
|
|
358
361
|
for (const dir of dirs) {
|
|
359
362
|
const phaseDir = node_path_1.default.join(phasesDir, dir);
|
|
360
|
-
const phaseMatch = dir.match(
|
|
363
|
+
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
361
364
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
362
365
|
let files;
|
|
363
366
|
try {
|
|
@@ -420,7 +423,7 @@ function scanVerificationGaps(planDir) {
|
|
|
420
423
|
const results = [];
|
|
421
424
|
for (const dir of dirs) {
|
|
422
425
|
const phaseDir = node_path_1.default.join(phasesDir, dir);
|
|
423
|
-
const phaseMatch = dir.match(
|
|
426
|
+
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
424
427
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
425
428
|
let files;
|
|
426
429
|
try {
|
|
@@ -475,7 +478,7 @@ function scanContextQuestions(planDir) {
|
|
|
475
478
|
const results = [];
|
|
476
479
|
for (const dir of dirs) {
|
|
477
480
|
const phaseDir = node_path_1.default.join(phasesDir, dir);
|
|
478
|
-
const phaseMatch = dir.match(
|
|
481
|
+
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
479
482
|
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
|
480
483
|
let files;
|
|
481
484
|
try {
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
* `gsd-core-` / `anthropic-` id prefix) is rejected.
|
|
19
19
|
* - Load-time re-gate (default-resilient): an overlay that fails validation or
|
|
20
20
|
* whose `engines.gsd` does not satisfy the running GSD version is SKIPPED
|
|
21
|
-
* with a warning — it never crashes the loop.
|
|
22
|
-
*
|
|
23
|
-
* `_overlay.incompatibleGateCapIds` so the loop
|
|
24
|
-
*
|
|
21
|
+
* with a warning — it never crashes the loop. A skipped capability that
|
|
22
|
+
* declares a `gate` is additionally recorded in
|
|
23
|
+
* `_overlay.incompatibleGateCapIds` / `_overlay.blockedGates` so the loop
|
|
24
|
+
* resolver can surface a loud fail-OPEN advisory for that gate (#2009): the
|
|
25
|
+
* un-evaluable gate is skipped (not enforced) with a remediation message,
|
|
26
|
+
* rather than silently vanishing.
|
|
25
27
|
*
|
|
26
28
|
* The merged registry is materialized by the canonical `buildRegistry`
|
|
27
29
|
* (re-exported from the generator, which ships) over a cap-map reconstructed
|
|
@@ -779,12 +781,12 @@ function loadRegistry(options = {}) {
|
|
|
779
781
|
// to load. Clear the map (the first-party base never lists overlay commandRoots — first-party
|
|
780
782
|
// command modules ship in bin/lib/, not via _overlay.commandRoots).
|
|
781
783
|
meta.commandRoots = {};
|
|
782
|
-
// #1461 OVL-2
|
|
783
|
-
//
|
|
784
|
-
// gate FAILS OPEN, violating ADR-1244 (a skipped capability declaring a gate must FAIL CLOSED).
|
|
784
|
+
// #1461 OVL-2 (HIGH): on compose failure the fallback DROPS every accepted overlay, so any
|
|
785
|
+
// accepted overlay that DECLARED a gate would have its gate silently vanish with no trace.
|
|
785
786
|
// Record each dropped gate-declaring overlay's gate as blocked using the SAME extraction the
|
|
786
|
-
// per-candidate `skip()` closure uses (gatePointsOf), so loop-resolver
|
|
787
|
-
//
|
|
787
|
+
// per-candidate `skip()` closure uses (gatePointsOf), so loop-resolver surfaces the loud
|
|
788
|
+
// fail-OPEN advisory (#2009) at each declared point exactly as it would for a per-candidate
|
|
789
|
+
// skip — the gate does not silently disappear.
|
|
788
790
|
for (const cap of overlayCaps) {
|
|
789
791
|
const gatePoints = gatePointsOf(cap);
|
|
790
792
|
if (gatePoints.length === 0)
|