@opengsd/gsd-core 1.8.0 → 1.9.1
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 +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -145,6 +145,10 @@ const COVERED = {
|
|
|
145
145
|
tests: [
|
|
146
146
|
'tests/frontmatter.property.test.cjs',
|
|
147
147
|
'tests/frontmatter.unit.test.cjs',
|
|
148
|
+
// #1882 added the unterminated-fence detection to frontmatter.cjs, and the tests that
|
|
149
|
+
// constrain it live here. Without this entry the mutants in that branch are covered by
|
|
150
|
+
// no test in the shard, so the module's score drops even though the behaviour is tested.
|
|
151
|
+
'tests/unusable-input.test.cjs',
|
|
148
152
|
],
|
|
149
153
|
minScore: 62,
|
|
150
154
|
},
|
|
@@ -102,6 +102,12 @@ ALLOWLIST=(
|
|
|
102
102
|
# exec command strings (execFileSync('npm', ['install'])) as test DATA the rule
|
|
103
103
|
# must lint — not attack vectors. ADR-1703 Phase 4 (#1726).
|
|
104
104
|
'tests/no-bare-npm-exec.rule.test.cjs'
|
|
105
|
+
# #2547 — the Kimi field-shadowing regression proves gsd-prompt-guard still
|
|
106
|
+
# SCANS the reconstructed edit[].new content when a model-supplied new_string
|
|
107
|
+
# tries to shadow it. The fixture must be a real injection phrase or the test
|
|
108
|
+
# asserts nothing: it is the payload the guard is required to catch, carried
|
|
109
|
+
# as test DATA. Same class as the read-injection-scanner suites above.
|
|
110
|
+
'tests/kimi-payload-field-shadowing.security.test.cjs'
|
|
105
111
|
)
|
|
106
112
|
|
|
107
113
|
is_allowlisted() {
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* scripts/registry-schema.cjs — pure schema/vocab constants + validation +
|
|
5
|
-
* markdown-generation logic for the
|
|
6
|
-
* (issue #2182):
|
|
5
|
+
* markdown-generation logic for the three third-party discoverability catalogs
|
|
6
|
+
* (issue #2182, plus #2904):
|
|
7
7
|
*
|
|
8
8
|
* - `docs/registries/capabilities.json` → "GSD Community Capability Registry"
|
|
9
9
|
* - `docs/registries/eos.json` → "GSD EoS Registry" (PR2)
|
|
10
|
+
* - `docs/registries/reviewers.json` → "GSD Reviewer Lane Registry" (issue #2904)
|
|
10
11
|
*
|
|
11
12
|
* The vocabulary constants below are ADDITIVE CONTRACTS that track the
|
|
12
13
|
* runtime/ADR closed vocabularies they describe — they are a documentation-
|
|
@@ -36,10 +37,19 @@
|
|
|
36
37
|
* (`{ namedDispatch, nested, maxDepth, background, subagentToolkit }`) —
|
|
37
38
|
* this registry accepts a free-form human summary string instead, so it
|
|
38
39
|
* carries the `AXES_FREE_STRING` sentinel rather than an enum array.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* `OPTIONAL_AXES` adds one further, OPTIONAL key on top of those eight:
|
|
41
|
+
* `effortSurface` (ADR-1239 amendment #2481). An entry may omit it
|
|
42
|
+
* (every entry published before the amendment stays valid) or declare
|
|
43
|
+
* it as `argv` | `none`, mirroring `HOST_INTEGRATION_AXES.effortSurface`
|
|
44
|
+
* in `src/host-integration.cts`.
|
|
45
|
+
* - `CAPABILITY_REQUIRED` / `EOS_REQUIRED` / `REVIEWER_REQUIRED` mirror the
|
|
46
|
+
* required top-level fields for each entry type, including `enginesGsd`
|
|
47
|
+
* (ADR-1244 D1 "Versioned capability manifest" — the `engines.gsd`
|
|
48
|
+
* semver-range gate, modelled on VS Code's `engines.vscode`).
|
|
49
|
+
* - `REVIEWER_LANE_TRANSPORTS` / `REVIEWER_EVIDENCE_CLASSES` /
|
|
50
|
+
* `REVIEWER_SLUG_RE` / `REVIEWER_FLAG_RE` / `REVIEWER_SECTION_MAX` mirror
|
|
51
|
+
* the ADR-2782 reviewer-lane vocabulary (`capability-validator.cjs`) for
|
|
52
|
+
* the `reviewer` entry type's `interactions` sub-object (issue #2904).
|
|
43
53
|
*
|
|
44
54
|
* This module is pure — no `fs`/`process`/child-process access — so tests
|
|
45
55
|
* can `require()` it directly and assert on structured return values.
|
|
@@ -90,37 +100,130 @@ const AXES = Object.freeze({
|
|
|
90
100
|
runtime: Object.freeze(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']),
|
|
91
101
|
});
|
|
92
102
|
|
|
103
|
+
// ─── ADR-1239 amendment #2481 — one additional, OPTIONAL negotiated axis ─────
|
|
104
|
+
// `effortSurface` was added to the runtime-descriptor vocabulary AFTER the
|
|
105
|
+
// original eight (`HOST_INTEGRATION_AXES.effortSurface` in
|
|
106
|
+
// `src/host-integration.cts`). It is kept OPTIONAL here — not folded into
|
|
107
|
+
// `AXES` — because registry entries mirror their upstream
|
|
108
|
+
// `registry/eos-entry.json` byte-for-byte, and requiring it would
|
|
109
|
+
// retroactively invalidate every entry published before the amendment.
|
|
110
|
+
// Values must match `HOST_INTEGRATION_AXES.effortSurface` exactly.
|
|
111
|
+
const OPTIONAL_AXES = Object.freeze({
|
|
112
|
+
effortSurface: Object.freeze(['argv', 'none']),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── ADR-2782 reviewer-lane vocabulary (issue #2904) ─────────────────────────
|
|
116
|
+
// A THIRD catalog: third-party reviewer lanes (`role: "reviewer"`, ADR-2782
|
|
117
|
+
// D3). A lane registers on ZERO Loop Extension Points and is forbidden from
|
|
118
|
+
// declaring `steps`/`contributions`/`gates`/`skills`/`agents`/`hooks`
|
|
119
|
+
// (`FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER`, capability-validator.cjs), so the
|
|
120
|
+
// Capability entry's two required `interactions` fields are unsatisfiable by
|
|
121
|
+
// construction for a lane — hence its own entry type rather than a relaxation
|
|
122
|
+
// of the Capability schema.
|
|
123
|
+
//
|
|
124
|
+
// These constants are ADDITIVE CONTRACTS mirroring the canonical runtime
|
|
125
|
+
// vocabulary in `gsd-core/bin/lib/capability-validator.cjs`, exactly the way
|
|
126
|
+
// `AXES` mirrors `HOST_INTEGRATION_AXES`. They are hand-written mirrors, NOT
|
|
127
|
+
// imports: this module is documented pure (no `fs`/`process`), and requiring a
|
|
128
|
+
// `gsd-core/bin/lib` runtime module from a docs-pipeline script would invert
|
|
129
|
+
// that. Parity is enforced instead by `tests/registry-reviewer-parity.test.cjs`.
|
|
130
|
+
//
|
|
131
|
+
// `REVIEWER_SLUG_RE` deliberately does NOT reuse the registry's kebab-case `id`
|
|
132
|
+
// grammar. `LANE_SLUG_RE` permits underscores AND a leading digit —
|
|
133
|
+
// `lm_studio`, `llama_cpp`, `4o-mini` are real shipped lane slugs — and
|
|
134
|
+
// capability-validator.cjs:807-810 requires the two grammars stay
|
|
135
|
+
// byte-identical. A kebab-only rule here would reject well-formed entries and
|
|
136
|
+
// leave authors with a schema satisfiable only by lying.
|
|
137
|
+
const REVIEWER_LANE_TRANSPORTS = Object.freeze(['spawn', 'openai-http']);
|
|
138
|
+
const REVIEWER_EVIDENCE_CLASSES = Object.freeze(['source-grounded', 'diff-only']);
|
|
139
|
+
const REVIEWER_SLUG_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
140
|
+
// Flags are kebab even when the slug is snake: `lm_studio` → `--lm-studio`.
|
|
141
|
+
const REVIEWER_FLAG_RE = /^--[a-z0-9][a-z0-9-]*$/;
|
|
142
|
+
// Cap for the one free-text reviewer interactions field, mirroring the 300-cap
|
|
143
|
+
// on the equivalently free-form `axes.dispatch`. A REVIEWS.md heading is short.
|
|
144
|
+
const REVIEWER_SECTION_MAX = 200;
|
|
145
|
+
|
|
93
146
|
// ─── Required top-level fields ───────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'author',
|
|
101
|
-
'
|
|
102
|
-
'enginesGsd',
|
|
103
|
-
'install',
|
|
104
|
-
'uninstall',
|
|
105
|
-
'interactions',
|
|
106
|
-
'discussion',
|
|
147
|
+
// The twelve fields every entry type requires. Each type's set is DERIVED from
|
|
148
|
+
// this one so a future shared field cannot be added to one type's list and
|
|
149
|
+
// silently forgotten in another (DEFECT.GENERATIVE-FIX). The three sets are
|
|
150
|
+
// distinct frozen arrays, not aliases, so a type may still diverge deliberately
|
|
151
|
+
// — as `eos` already does with `protocolVersion`.
|
|
152
|
+
const BASE_REQUIRED = Object.freeze([
|
|
153
|
+
'id', 'name', 'type', 'repo', 'description', 'author', 'license',
|
|
154
|
+
'enginesGsd', 'install', 'uninstall', 'interactions', 'discussion',
|
|
107
155
|
]);
|
|
156
|
+
const CAPABILITY_REQUIRED = Object.freeze([...BASE_REQUIRED]);
|
|
157
|
+
const EOS_REQUIRED = Object.freeze([...BASE_REQUIRED, 'protocolVersion']);
|
|
158
|
+
// A lane is installed with `gsd capability install`, owns a repo, a license and
|
|
159
|
+
// an `engines.gsd` range exactly as a Feature Capability does — so it requires
|
|
160
|
+
// the same twelve top-level fields. Only `interactions` differs.
|
|
161
|
+
const REVIEWER_REQUIRED = Object.freeze([...BASE_REQUIRED]);
|
|
162
|
+
|
|
163
|
+
// Control-character rejection (defense in depth): `allowTabNewline` widens the
|
|
164
|
+
// reject-set exception for the two shell-snippet fields (install/uninstall),
|
|
165
|
+
// which legitimately contain tabs/newlines; every other free text field
|
|
166
|
+
// disallows ALL C0 control characters plus DEL (incl. \n/\t). Checked via char
|
|
167
|
+
// codes (not a literal control-char regex range) — same approach as
|
|
168
|
+
// capability-validator.cjs's hooks[].matcher check, which avoids tripping
|
|
169
|
+
// ESLint's no-control-regex rule. Module-scope so both the top-level field
|
|
170
|
+
// checks inside `validateEntries` and the `interactions` sub-object
|
|
171
|
+
// validators (module-level functions, outside that closure) share the ONE
|
|
172
|
+
// implementation rather than each keeping their own copy.
|
|
173
|
+
function hasDisallowedControlChar(v, allowTabNewline) {
|
|
174
|
+
for (let c = 0; c < v.length; c += 1) {
|
|
175
|
+
const code = v.charCodeAt(c);
|
|
176
|
+
if (allowTabNewline && (code === 0x09 || code === 0x0a)) continue;
|
|
177
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
108
181
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
182
|
+
// Caps for `interactions` array-of-strings fields (configKeys, requires,
|
|
183
|
+
// runtimeCompat, produces, consumes, requiresBinaries, ...). These bound
|
|
184
|
+
// UNTRUSTED third-party strings that are rendered verbatim (after mdInline
|
|
185
|
+
// escaping) into a committed Markdown catalog — an unbounded count or length
|
|
186
|
+
// lets a malicious registry PR blow up the generated doc.
|
|
187
|
+
const INTERACTION_STRING_MAX = 200;
|
|
188
|
+
const INTERACTION_ARRAY_MAX = 50;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Validate an interactions field that is an array of free-form untrusted
|
|
192
|
+
* strings: shape, element count, per-element length, and control characters.
|
|
193
|
+
* `allowEmpty` distinguishes "may be empty" fields from non-empty-required
|
|
194
|
+
* ones — non-empty-required fields' blank-array message is expected to be
|
|
195
|
+
* handled by the caller (this helper does not special-case emptiness itself
|
|
196
|
+
* beyond letting an empty array with `allowEmpty: true` through).
|
|
197
|
+
*
|
|
198
|
+
* @param {object} interactions
|
|
199
|
+
* @param {string} field
|
|
200
|
+
* @param {(field: string, reason: string) => void} addError
|
|
201
|
+
* @param {{allowEmpty?: boolean}} [opts]
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
function validateStringArrayField(interactions, field, addError, { allowEmpty = true } = {}) {
|
|
205
|
+
const v = interactions[field];
|
|
206
|
+
const qualifiedField = `interactions.${field}`;
|
|
207
|
+
|
|
208
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
|
|
209
|
+
addError(qualifiedField, 'must be an array of strings');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!allowEmpty && v.length === 0) return;
|
|
214
|
+
|
|
215
|
+
if (v.length > INTERACTION_ARRAY_MAX) {
|
|
216
|
+
addError(qualifiedField, `exceeds max entries ${INTERACTION_ARRAY_MAX}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const x of v) {
|
|
220
|
+
if (x.length > INTERACTION_STRING_MAX) {
|
|
221
|
+
addError(qualifiedField, `exceeds max length ${INTERACTION_STRING_MAX}`);
|
|
222
|
+
} else if (hasDisallowedControlChar(x, false)) {
|
|
223
|
+
addError(qualifiedField, 'must not contain control characters');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
124
227
|
|
|
125
228
|
// Escape Markdown inline metacharacters in UNTRUSTED free text so a registry
|
|
126
229
|
// entry cannot inject links/tables/code-spans into the generated catalog.
|
|
@@ -204,10 +307,7 @@ function validateCapabilityInteractions(interactions, addError) {
|
|
|
204
307
|
|
|
205
308
|
for (const field of ['configKeys', 'requires', 'runtimeCompat', 'produces', 'consumes']) {
|
|
206
309
|
if (interactions[field] === undefined) continue;
|
|
207
|
-
|
|
208
|
-
if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
|
|
209
|
-
addError(`interactions.${field}`, 'must be an array of strings');
|
|
210
|
-
}
|
|
310
|
+
validateStringArrayField(interactions, field, addError);
|
|
211
311
|
}
|
|
212
312
|
}
|
|
213
313
|
|
|
@@ -246,15 +346,39 @@ function validateEosInteractions(interactions, addError) {
|
|
|
246
346
|
if (typeof axes !== 'object' || axes === null || Array.isArray(axes)) {
|
|
247
347
|
addError('interactions.axes', 'axes must be an object');
|
|
248
348
|
} else {
|
|
249
|
-
const
|
|
349
|
+
const requiredKeys = Object.keys(AXES);
|
|
350
|
+
const optionalKeys = Object.keys(OPTIONAL_AXES);
|
|
250
351
|
const actualKeys = Object.keys(axes);
|
|
251
352
|
const actualKeySet = new Set(actualKeys);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
353
|
+
|
|
354
|
+
// Every AXES key is mandatory; an extra key is tolerated ONLY when it is
|
|
355
|
+
// a recognized OPTIONAL_AXES key (currently just `effortSurface`) — any
|
|
356
|
+
// other extra key is still rejected as unknown.
|
|
357
|
+
const missingRequiredKeys = requiredKeys.filter((k) => !actualKeySet.has(k));
|
|
358
|
+
const unknownKeys = actualKeys.filter((k) => !requiredKeys.includes(k) && !optionalKeys.includes(k));
|
|
359
|
+
|
|
360
|
+
if (missingRequiredKeys.length > 0) {
|
|
361
|
+
addError('interactions.axes', `axes is missing required key(s): ${missingRequiredKeys.join(', ')}`);
|
|
362
|
+
}
|
|
363
|
+
if (unknownKeys.length > 0) {
|
|
364
|
+
addError('interactions.axes', `axes has unknown key(s): ${unknownKeys.join(', ')}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Only validate individual values once the key set itself is sound —
|
|
368
|
+
// mirrors the original gate (values were never checked against a
|
|
369
|
+
// malformed key set either).
|
|
370
|
+
if (missingRequiredKeys.length === 0 && unknownKeys.length === 0) {
|
|
371
|
+
for (const key of actualKeys) {
|
|
372
|
+
// Inline literal guards — CodeQL barrier pattern. Reaching here already
|
|
373
|
+
// implies `key` is one of the nine literal axis names (the unknown-key
|
|
374
|
+
// gate above rejected everything else), so this is unreachable in
|
|
375
|
+
// practice; it is written inline anyway because CodeQL cannot follow
|
|
376
|
+
// that gate across the `.includes()` filter and would otherwise flag
|
|
377
|
+
// the bracket reads below as prototype-pollution sinks.
|
|
378
|
+
if (key === '__proto__') continue;
|
|
379
|
+
if (key === 'constructor') continue;
|
|
380
|
+
if (key === 'prototype') continue;
|
|
381
|
+
const allowedValues = Object.hasOwn(AXES, key) ? AXES[key] : OPTIONAL_AXES[key];
|
|
258
382
|
const v = axes[key];
|
|
259
383
|
if (allowedValues === AXES_FREE_STRING) {
|
|
260
384
|
if (typeof v !== 'string' || v.trim() === '') {
|
|
@@ -271,12 +395,93 @@ function validateEosInteractions(interactions, addError) {
|
|
|
271
395
|
}
|
|
272
396
|
}
|
|
273
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Validate the `interactions` sub-object for a reviewer entry (ADR-2782 D3
|
|
400
|
+
* lane vocabulary — issue #2904).
|
|
401
|
+
*
|
|
402
|
+
* @param {object} interactions
|
|
403
|
+
* @param {(field: string, reason: string) => void} addError
|
|
404
|
+
* @returns {void}
|
|
405
|
+
*/
|
|
406
|
+
function validateReviewerInteractions(interactions, addError) {
|
|
407
|
+
const allowedKeys = new Set([
|
|
408
|
+
'slug',
|
|
409
|
+
'flags',
|
|
410
|
+
'transport',
|
|
411
|
+
'evidenceClass',
|
|
412
|
+
'reviewsSection',
|
|
413
|
+
'requiresBinaries',
|
|
414
|
+
'configKeys',
|
|
415
|
+
'runtimeCompat',
|
|
416
|
+
]);
|
|
417
|
+
for (const key of Object.keys(interactions)) {
|
|
418
|
+
if (!allowedKeys.has(key)) addError(`interactions.${key}`, 'unknown field');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const field of allowedKeys) {
|
|
422
|
+
if (interactions[field] === undefined) addError(`interactions.${field}`, 'missing required field');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (interactions.slug !== undefined) {
|
|
426
|
+
const v = interactions.slug;
|
|
427
|
+
if (typeof v !== 'string' || !REVIEWER_SLUG_RE.test(v)) {
|
|
428
|
+
addError('interactions.slug', 'must match the reviewer lane slug grammar');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (interactions.flags !== undefined) {
|
|
433
|
+
const v = interactions.flags;
|
|
434
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((x) => typeof x === 'string' && REVIEWER_FLAG_RE.test(x))) {
|
|
435
|
+
addError('interactions.flags', 'must be a non-empty array of lane CLI flags');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (interactions.transport !== undefined) {
|
|
440
|
+
const v = interactions.transport;
|
|
441
|
+
if (typeof v !== 'string' || !REVIEWER_LANE_TRANSPORTS.includes(v)) {
|
|
442
|
+
addError('interactions.transport', 'must be one of the allowed lane transports');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (interactions.evidenceClass !== undefined) {
|
|
447
|
+
const v = interactions.evidenceClass;
|
|
448
|
+
if (typeof v !== 'string' || !REVIEWER_EVIDENCE_CLASSES.includes(v)) {
|
|
449
|
+
addError('interactions.evidenceClass', 'must be one of the allowed evidence classes');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (interactions.reviewsSection !== undefined) {
|
|
454
|
+
const v = interactions.reviewsSection;
|
|
455
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
456
|
+
addError('interactions.reviewsSection', 'must be a non-empty string');
|
|
457
|
+
} else if (v.length > REVIEWER_SECTION_MAX) {
|
|
458
|
+
addError('interactions.reviewsSection', `exceeds max length ${REVIEWER_SECTION_MAX}`);
|
|
459
|
+
} else if (hasDisallowedControlChar(v, false)) {
|
|
460
|
+
addError('interactions.reviewsSection', 'must not contain control characters');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const field of ['requiresBinaries', 'configKeys', 'runtimeCompat']) {
|
|
465
|
+
if (interactions[field] === undefined) continue;
|
|
466
|
+
validateStringArrayField(interactions, field, addError);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Per-type rules. A Map (not a plain object) so the lookup below is not a
|
|
471
|
+
// bracket-read on a caller-supplied key — that shape reads as a
|
|
472
|
+
// prototype-pollution sink to CodeQL, and a Map.get does not.
|
|
473
|
+
const TYPE_RULES = new Map([
|
|
474
|
+
['capability', { required: CAPABILITY_REQUIRED, validateInteractions: validateCapabilityInteractions }],
|
|
475
|
+
['eos', { required: EOS_REQUIRED, validateInteractions: validateEosInteractions }],
|
|
476
|
+
['reviewer', { required: REVIEWER_REQUIRED, validateInteractions: validateReviewerInteractions }],
|
|
477
|
+
]);
|
|
478
|
+
|
|
274
479
|
/**
|
|
275
480
|
* Validate an array of registry entries against the closed schema for
|
|
276
|
-
* `opts.type` ('capability' | 'eos').
|
|
481
|
+
* `opts.type` ('capability' | 'eos' | 'reviewer').
|
|
277
482
|
*
|
|
278
483
|
* @param {object[]} entries
|
|
279
|
-
* @param {{type: 'capability'|'eos'}} opts
|
|
484
|
+
* @param {{type: 'capability'|'eos'|'reviewer'}} opts
|
|
280
485
|
* @returns {{ok: boolean, errors: Array<{index: number, id?: string, field: string, reason: string}>}}
|
|
281
486
|
*/
|
|
282
487
|
function validateEntries(entries, opts) {
|
|
@@ -284,13 +489,22 @@ function validateEntries(entries, opts) {
|
|
|
284
489
|
return { ok: false, errors: [{ index: -1, field: '(root)', reason: 'entries must be an array' }] };
|
|
285
490
|
}
|
|
286
491
|
|
|
492
|
+
// An unrecognized type is a hard error, not a silent fallthrough. Before the
|
|
493
|
+
// third type existed this was a binary ternary whose ELSE branch was
|
|
494
|
+
// `capability`, so a typo'd type validated against the wrong schema and
|
|
495
|
+
// reported plausible-looking per-entry errors.
|
|
496
|
+
const rules = TYPE_RULES.get(opts.type);
|
|
497
|
+
if (!rules) {
|
|
498
|
+
return { ok: false, errors: [{ index: -1, field: '(root)', reason: `unknown registry type "${opts.type}"` }] };
|
|
499
|
+
}
|
|
500
|
+
|
|
287
501
|
// Entry-count cap: a pathologically large array (e.g. from an automated or
|
|
288
502
|
// malicious PR) is rejected wholesale rather than validated entry-by-entry.
|
|
289
503
|
if (entries.length > 2000) {
|
|
290
504
|
return { ok: false, errors: [{ index: -1, field: '(root)', reason: 'too many entries (max 2000)' }] };
|
|
291
505
|
}
|
|
292
506
|
|
|
293
|
-
const required =
|
|
507
|
+
const required = rules.required;
|
|
294
508
|
const requiredSet = new Set(required);
|
|
295
509
|
const seenIds = new Set();
|
|
296
510
|
const errors = [];
|
|
@@ -322,21 +536,9 @@ function validateEntries(entries, opts) {
|
|
|
322
536
|
}
|
|
323
537
|
}
|
|
324
538
|
|
|
325
|
-
// Control-character rejection (defense in depth)
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// text field disallows ALL C0 control characters plus DEL (incl. \n/\t).
|
|
329
|
-
// Checked via char codes (not a literal control-char regex range) — same
|
|
330
|
-
// approach as capability-validator.cjs's hooks[].matcher check, which
|
|
331
|
-
// avoids tripping ESLint's no-control-regex rule.
|
|
332
|
-
const hasDisallowedControlChar = (v, allowTabNewline) => {
|
|
333
|
-
for (let c = 0; c < v.length; c += 1) {
|
|
334
|
-
const code = v.charCodeAt(c);
|
|
335
|
-
if (allowTabNewline && (code === 0x09 || code === 0x0a)) continue;
|
|
336
|
-
if (code < 0x20 || code === 0x7f) return true;
|
|
337
|
-
}
|
|
338
|
-
return false;
|
|
339
|
-
};
|
|
539
|
+
// Control-character rejection (defense in depth) — delegates to the
|
|
540
|
+
// module-scope `hasDisallowedControlChar` (shared with the `interactions`
|
|
541
|
+
// sub-object validators below) so there is exactly one implementation.
|
|
340
542
|
const checkNoControlChars = (field, allowTabNewline) => {
|
|
341
543
|
if (missing.has(field)) return;
|
|
342
544
|
const v = entry[field];
|
|
@@ -419,10 +621,8 @@ function validateEntries(entries, opts) {
|
|
|
419
621
|
const interactions = entry.interactions;
|
|
420
622
|
if (typeof interactions !== 'object' || interactions === null || Array.isArray(interactions)) {
|
|
421
623
|
addError('interactions', 'interactions must be an object');
|
|
422
|
-
} else if (opts.type === 'eos') {
|
|
423
|
-
validateEosInteractions(interactions, addError);
|
|
424
624
|
} else {
|
|
425
|
-
|
|
625
|
+
rules.validateInteractions(interactions, addError);
|
|
426
626
|
}
|
|
427
627
|
}
|
|
428
628
|
|
|
@@ -436,12 +636,92 @@ function validateEntries(entries, opts) {
|
|
|
436
636
|
return { ok: errors.length === 0, errors };
|
|
437
637
|
}
|
|
438
638
|
|
|
639
|
+
// Per-type page presentation AND per-type interaction summary both live in
|
|
640
|
+
// this ONE table (Map, for the same CodeQL reason as TYPE_RULES): title/
|
|
641
|
+
// addNoun drive the page header, buildSummary drives the per-entry "Every
|
|
642
|
+
// interaction with GSD" line. Folding both into a single lookup means a
|
|
643
|
+
// future fourth registry type MUST supply its own buildSummary or the
|
|
644
|
+
// `RENDER_META.get` miss below throws — it cannot silently inherit
|
|
645
|
+
// capability's (or any other type's) rendering the way the old if/else-if/
|
|
646
|
+
// else chain's final `else` branch used to.
|
|
647
|
+
const RENDER_META = new Map([
|
|
648
|
+
[
|
|
649
|
+
'capability',
|
|
650
|
+
{
|
|
651
|
+
title: 'GSD Community Capability Registry',
|
|
652
|
+
addNoun: 'capability',
|
|
653
|
+
buildSummary(entry, interactions) {
|
|
654
|
+
let summary =
|
|
655
|
+
`Loop Extension Points: ${(interactions.loopExtensionPoints || []).join(', ')}; ` +
|
|
656
|
+
`hook kinds: ${(interactions.hookKinds || []).join(', ')}`;
|
|
657
|
+
for (const field of ['configKeys', 'requires', 'runtimeCompat', 'produces', 'consumes']) {
|
|
658
|
+
const v = interactions[field];
|
|
659
|
+
if (Array.isArray(v) && v.length > 0) summary += `; ${field}: ${v.join(', ')}`;
|
|
660
|
+
}
|
|
661
|
+
// configKeys/requires/runtimeCompat/produces/consumes are untrusted
|
|
662
|
+
// free-form strings (schema only requires "array of strings") — same
|
|
663
|
+
// single-pass mdInline rationale as the eos branch above.
|
|
664
|
+
return summary;
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
[
|
|
669
|
+
'eos',
|
|
670
|
+
{
|
|
671
|
+
title: 'GSD EoS Registry',
|
|
672
|
+
addNoun: 'integration',
|
|
673
|
+
buildSummary(entry, interactions) {
|
|
674
|
+
// Required AXES keys always render, in their fixed order; an OPTIONAL_AXES
|
|
675
|
+
// key (e.g. `effortSurface`) renders ONLY when the entry actually carries
|
|
676
|
+
// it — an entry that omits it must render byte-identical to before
|
|
677
|
+
// OPTIONAL_AXES existed (no `effortSurface=undefined` noise).
|
|
678
|
+
const presentOptionalKeys = Object.keys(OPTIONAL_AXES).filter(
|
|
679
|
+
(key) => interactions.axes && Object.hasOwn(interactions.axes, key),
|
|
680
|
+
);
|
|
681
|
+
const axesSummary = [...Object.keys(AXES), ...presentOptionalKeys]
|
|
682
|
+
.map((key) => `${key}=${interactions.axes ? interactions.axes[key] : undefined}`)
|
|
683
|
+
.join(', ');
|
|
684
|
+
return (
|
|
685
|
+
`Interface points: ${(interactions.interfacePoints || []).join(', ')}; ` +
|
|
686
|
+
`profile: ${interactions.profile}; protocol v${entry.protocolVersion}; axes: ${axesSummary}`
|
|
687
|
+
);
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
[
|
|
692
|
+
'reviewer',
|
|
693
|
+
{
|
|
694
|
+
title: 'GSD Reviewer Lane Registry',
|
|
695
|
+
addNoun: 'reviewer lane',
|
|
696
|
+
buildSummary(entry, interactions) {
|
|
697
|
+
let summary =
|
|
698
|
+
`Lane: ${interactions.slug}; ` +
|
|
699
|
+
`flags: ${(interactions.flags || []).join(', ')}; ` +
|
|
700
|
+
`transport: ${interactions.transport}; ` +
|
|
701
|
+
`evidence: ${interactions.evidenceClass}; ` +
|
|
702
|
+
`REVIEWS.md section: ${interactions.reviewsSection}`;
|
|
703
|
+
for (const field of ['requiresBinaries', 'configKeys', 'runtimeCompat']) {
|
|
704
|
+
const v = interactions[field];
|
|
705
|
+
if (Array.isArray(v) && v.length > 0) summary += `; ${field}: ${v.join(', ')}`;
|
|
706
|
+
}
|
|
707
|
+
// slug/flags/transport are vocab-constrained; reviewsSection and the
|
|
708
|
+
// three arrays are untrusted free text — same single-pass mdInline
|
|
709
|
+
// rationale as the eos/capability branches above: none of the literal
|
|
710
|
+
// separator text contains Markdown metacharacters, so one pass over the
|
|
711
|
+
// assembled summary neutralizes every embedded value.
|
|
712
|
+
return summary;
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
]);
|
|
717
|
+
|
|
439
718
|
/**
|
|
440
719
|
* Render the deterministic Markdown document for a registry.
|
|
441
720
|
*
|
|
442
721
|
* @param {object[]} entries
|
|
443
|
-
* @param {{type: 'capability'|'eos', sourceFile?: string}} opts
|
|
722
|
+
* @param {{type: 'capability'|'eos'|'reviewer', sourceFile?: string}} opts
|
|
444
723
|
* @returns {string}
|
|
724
|
+
* @throws {Error} when opts.type is not a known registry type
|
|
445
725
|
*/
|
|
446
726
|
function renderMarkdown(entries, opts) {
|
|
447
727
|
const sorted = [...entries].sort((a, b) => {
|
|
@@ -450,19 +730,27 @@ function renderMarkdown(entries, opts) {
|
|
|
450
730
|
return 0;
|
|
451
731
|
});
|
|
452
732
|
const isEos = opts.type === 'eos';
|
|
733
|
+
// An unrecognized type must fail loudly rather than silently render a
|
|
734
|
+
// "GSD Community Capability Registry" page — mirroring the validateEntries
|
|
735
|
+
// unknown-type guard above. This function writes a COMMITTED catalog file,
|
|
736
|
+
// so a silent wrong-title render is the worst failure mode available.
|
|
737
|
+
// Message shape mirrors gen-registry.cjs#renderFor's existing
|
|
738
|
+
// `gen-registry: unknown registry type "..."` throw.
|
|
739
|
+
const meta = RENDER_META.get(opts.type);
|
|
740
|
+
if (!meta) throw new Error(`registry-schema: unknown registry type "${opts.type}"`);
|
|
453
741
|
const lines = [];
|
|
454
742
|
|
|
455
743
|
lines.push(
|
|
456
744
|
`<!-- GENERATED by scripts/gen-registry.cjs from docs/registries/${opts.sourceFile} — do not edit by hand; run \`npm run gen:registry\` -->`,
|
|
457
745
|
);
|
|
458
746
|
lines.push('');
|
|
459
|
-
lines.push(
|
|
747
|
+
lines.push(`# ${meta.title}`);
|
|
460
748
|
lines.push('');
|
|
461
749
|
lines.push(
|
|
462
750
|
"> **Not an endorsement.** Inclusion means only that a maintainer merged a PR linking the author's repository — GSD has not reviewed, tested, or verified any listing. See the [registry README](./README.md).",
|
|
463
751
|
);
|
|
464
752
|
lines.push('');
|
|
465
|
-
lines.push(`_To add your ${
|
|
753
|
+
lines.push(`_To add your ${meta.addNoun}, see the [registry README](./README.md)._`);
|
|
466
754
|
lines.push('');
|
|
467
755
|
|
|
468
756
|
if (sorted.length === 0) {
|
|
@@ -495,31 +783,12 @@ function renderMarkdown(entries, opts) {
|
|
|
495
783
|
lines.push(`- **What it is:** ${mdInline(entry.description)}`);
|
|
496
784
|
lines.push(`- **Author:** ${mdInline(entry.author)}`);
|
|
497
785
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
`profile: ${interactions.profile}; protocol v${entry.protocolVersion}; axes: ${axesSummary}`;
|
|
505
|
-
// Single mdInline pass over the fully-assembled summary: none of the
|
|
506
|
-
// literal separator text above contains Markdown metacharacters, so
|
|
507
|
-
// this equally neutralizes every embedded free-text/vocab value
|
|
508
|
-
// (notably interactions.axes.dispatch, a free-form untrusted string).
|
|
509
|
-
lines.push(`- **Every interaction with GSD:** ${mdInline(summary)}`);
|
|
510
|
-
} else {
|
|
511
|
-
let summary =
|
|
512
|
-
`Loop Extension Points: ${(interactions.loopExtensionPoints || []).join(', ')}; ` +
|
|
513
|
-
`hook kinds: ${(interactions.hookKinds || []).join(', ')}`;
|
|
514
|
-
for (const field of ['configKeys', 'requires', 'runtimeCompat', 'produces', 'consumes']) {
|
|
515
|
-
const v = interactions[field];
|
|
516
|
-
if (Array.isArray(v) && v.length > 0) summary += `; ${field}: ${v.join(', ')}`;
|
|
517
|
-
}
|
|
518
|
-
// configKeys/requires/runtimeCompat/produces/consumes are untrusted
|
|
519
|
-
// free-form strings (schema only requires "array of strings") — same
|
|
520
|
-
// single-pass mdInline rationale as the eos branch above.
|
|
521
|
-
lines.push(`- **Every interaction with GSD:** ${mdInline(summary)}`);
|
|
522
|
-
}
|
|
786
|
+
// Single mdInline pass over the fully-assembled per-type summary: none of
|
|
787
|
+
// the literal separator text in any RENDER_META buildSummary implementation
|
|
788
|
+
// contains Markdown metacharacters, so one pass over the assembled string
|
|
789
|
+
// equally neutralizes every embedded free-text/vocab value (notably eos's
|
|
790
|
+
// interactions.axes.dispatch, a free-form untrusted string).
|
|
791
|
+
lines.push(`- **Every interaction with GSD:** ${mdInline(meta.buildSummary(entry, interactions))}`);
|
|
523
792
|
|
|
524
793
|
// Code-span content (install/uninstall) is NOT mdInline-escaped — it is a
|
|
525
794
|
// verbatim shell snippet, not inline prose. Instead each block picks a
|
|
@@ -556,9 +825,18 @@ module.exports = {
|
|
|
556
825
|
INTERFACE_POINTS,
|
|
557
826
|
PROFILES,
|
|
558
827
|
AXES,
|
|
828
|
+
OPTIONAL_AXES,
|
|
559
829
|
AXES_FREE_STRING,
|
|
560
830
|
CAPABILITY_REQUIRED,
|
|
561
831
|
EOS_REQUIRED,
|
|
832
|
+
REVIEWER_REQUIRED,
|
|
833
|
+
REVIEWER_LANE_TRANSPORTS,
|
|
834
|
+
REVIEWER_EVIDENCE_CLASSES,
|
|
835
|
+
REVIEWER_SLUG_RE,
|
|
836
|
+
REVIEWER_FLAG_RE,
|
|
837
|
+
REVIEWER_SECTION_MAX,
|
|
838
|
+
INTERACTION_STRING_MAX,
|
|
839
|
+
INTERACTION_ARRAY_MAX,
|
|
562
840
|
isValidGsdRange,
|
|
563
841
|
validateEntries,
|
|
564
842
|
renderMarkdown,
|
|
@@ -30,18 +30,36 @@ const HEADER_RE = /^([a-z]+)(\([^)]*\))?(!)?:/i;
|
|
|
30
30
|
// An issue reference inside a scope: `(#123)`, `(#123, core)`, etc.
|
|
31
31
|
const ISSUE_REF_IN_SCOPE_RE = /#\d+/;
|
|
32
32
|
|
|
33
|
+
// #2716: recognized non-user-facing conventional-commit types. A title whose
|
|
34
|
+
// START-anchored type prefix is one of these is internal work (tests, chores,
|
|
35
|
+
// CI, docs, refactors, perf, reverts) and must NOT render under the user-facing
|
|
36
|
+
// "Enhancement" heading in release notes. Only a CLEAN prefix match qualifies —
|
|
37
|
+
// untyped or anchor-defeated titles fall through to the visible Enhancement
|
|
38
|
+
// fallback (a safety net so possibly-user-facing content is never hidden).
|
|
39
|
+
const NON_USER_FACING_TYPES = new Set([
|
|
40
|
+
'docs', 'refactor', 'test', 'ci', 'chore', 'perf', 'revert',
|
|
41
|
+
]);
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Classify a clean conventional title into a changelog bucket.
|
|
35
45
|
* Callers that hold a full changelog bullet line (with a `* ` marker and a
|
|
36
46
|
* ` by @author` suffix) must strip those first; this operates on the title.
|
|
37
47
|
*
|
|
38
48
|
* @param {string} title
|
|
39
|
-
* @returns {'Feature'|'Fix'|'Enhancement'}
|
|
49
|
+
* @returns {'Feature'|'Fix'|'Enhancement'|'Internal'}
|
|
40
50
|
*/
|
|
41
51
|
function classifyBucket(title) {
|
|
42
52
|
const t = String(title == null ? '' : title).trim();
|
|
43
53
|
if (FEATURE_RE.test(t)) return 'Feature';
|
|
44
54
|
if (FIX_RE.test(t)) return 'Fix';
|
|
55
|
+
// #2716: a clean non-user-facing type prefix → Internal (omitted from user-facing
|
|
56
|
+
// release-note sections). The HEADER_RE anchor ensures a leading tag/prefix
|
|
57
|
+
// (e.g. `[security] fix(...)`) does NOT match here — those keep falling through
|
|
58
|
+
// to the visible Enhancement fallback.
|
|
59
|
+
const headerMatch = HEADER_RE.exec(t);
|
|
60
|
+
if (headerMatch && NON_USER_FACING_TYPES.has(headerMatch[1].toLowerCase())) {
|
|
61
|
+
return 'Internal';
|
|
62
|
+
}
|
|
45
63
|
return 'Enhancement';
|
|
46
64
|
}
|
|
47
65
|
|