@opengsd/gsd-core 1.7.0 → 1.9.0
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 +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- 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 +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- 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 +1450 -160
- 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/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- 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 +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- 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/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 +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- 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 +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- 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 +4 -15
- 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/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -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/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- 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/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -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/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -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/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- 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 +97 -9
- 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-context-monitor.js +27 -9
- 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 +97 -9
- 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 +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- 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-test-timings.cjs +201 -0
- 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-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -71,6 +71,7 @@ const {
|
|
|
71
71
|
validateArtifactKindEntry,
|
|
72
72
|
validateArtifactLayout,
|
|
73
73
|
validateRuntimeBody,
|
|
74
|
+
collectReviewerWarnings,
|
|
74
75
|
materializeHookFragments,
|
|
75
76
|
validateAgainstContract,
|
|
76
77
|
validateConsumesGlobal,
|
|
@@ -128,6 +129,71 @@ function loadCentralConfigKeys(schemaPath = CONFIG_SCHEMA_PATH) {
|
|
|
128
129
|
return new Set(Array.isArray(manifest.validKeys) ? manifest.validKeys : []);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Loads the central config-schema's DYNAMIC key patterns (#2797).
|
|
134
|
+
*
|
|
135
|
+
* `loadCentralConfigKeys` above reads `validKeys` only, so a federated key
|
|
136
|
+
* claimed by a central *pattern* was invisible to the exclusivity check. That is
|
|
137
|
+
* not a cosmetic gap: `isCentralConfigKey` consults these same patterns, and
|
|
138
|
+
* `mergeFederatedConfig` skips every key for which it returns true — so an
|
|
139
|
+
* overlapping slice is inert while the build stays green.
|
|
140
|
+
*
|
|
141
|
+
* The patterns are read from the SAME manifest the runtime reads and compiled
|
|
142
|
+
* with the same `source`, rather than re-implementing a matcher here, so the two
|
|
143
|
+
* cannot drift.
|
|
144
|
+
*
|
|
145
|
+
* Failure contract MATCHES `loadCentralConfigKeys` above deliberately — the two
|
|
146
|
+
* read the same file and must not disagree about what a broken one means:
|
|
147
|
+
* - ENOENT → empty list. Legitimately absent.
|
|
148
|
+
* - Any other read error, or a JSON parse error → prominent stderr + throw.
|
|
149
|
+
*
|
|
150
|
+
* An earlier revision swallowed the parse error and returned []. That is
|
|
151
|
+
* fail-OPEN on the gate this function exists to feed: with zero patterns,
|
|
152
|
+
* `validateCrossCapability`'s pattern-collision check silently passes and an
|
|
153
|
+
* inert federated slice ships green. It was masked in the one production call
|
|
154
|
+
* site only because `loadCentralConfigKeys` runs first against the same path and
|
|
155
|
+
* throws — a coincidence of ordering, not a guarantee, and this function is
|
|
156
|
+
* exported and called standalone.
|
|
157
|
+
*
|
|
158
|
+
* A single unparseable PATTERN is still skipped rather than fatal: that is a
|
|
159
|
+
* per-entry defect the central schema's own tests own, and skipping one pattern
|
|
160
|
+
* degrades to "checked less" rather than blocking every build.
|
|
161
|
+
*/
|
|
162
|
+
function loadCentralConfigPatterns(schemaPath = CONFIG_SCHEMA_PATH) {
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = fs.readFileSync(schemaPath, 'utf8');
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err && err.code === 'ENOENT') return [];
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
' ERROR Failed to read config-schema manifest at ' + schemaPath + ': ' + err.message + '\n',
|
|
170
|
+
);
|
|
171
|
+
throw new ExitError(1, 'could not read config-schema manifest');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let manifest;
|
|
175
|
+
try {
|
|
176
|
+
manifest = JSON.parse(raw);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
' ERROR Config-schema manifest at ' + schemaPath + ' is broken (JSON parse error): ' + err.message + '\n',
|
|
180
|
+
);
|
|
181
|
+
throw new ExitError(1, 'config-schema manifest JSON is malformed');
|
|
182
|
+
}
|
|
183
|
+
const declared = Array.isArray(manifest.dynamicKeyPatterns) ? manifest.dynamicKeyPatterns : [];
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const entry of declared) {
|
|
186
|
+
const src = entry && typeof entry.source === 'string' ? entry.source : null;
|
|
187
|
+
if (!src) continue;
|
|
188
|
+
try {
|
|
189
|
+
out.push(new RegExp(src));
|
|
190
|
+
} catch {
|
|
191
|
+
// Unparseable pattern — the central schema's own tests own that failure.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
131
197
|
// ─── ADR-857 Phase 4a: Derived views ─────────────────────────────────────────
|
|
132
198
|
|
|
133
199
|
// (Config-slice validation, per-capability validators, contract validators,
|
|
@@ -335,14 +401,21 @@ function runConsistencyGate(capabilityClusters, profileMembership, capMap) {
|
|
|
335
401
|
* (used during 3a-impl while migration is in-progress).
|
|
336
402
|
* @param {string} [capabilitiesDir] Override capabilities dir (for testing with fixtures).
|
|
337
403
|
*/
|
|
338
|
-
function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
404
|
+
function loadAndValidate(centralKeys, capabilitiesDir, centralPatterns) {
|
|
339
405
|
const resolvedCentralKeys = centralKeys !== undefined ? centralKeys : loadCentralConfigKeys();
|
|
406
|
+
// #2797: patterns default to the real manifest unless a caller passes its own
|
|
407
|
+
// (tests pass [] to isolate the exact-key path).
|
|
408
|
+
const resolvedCentralPatterns = centralPatterns !== undefined ? centralPatterns : loadCentralConfigPatterns();
|
|
340
409
|
const resolvedCapDir = capabilitiesDir !== undefined ? capabilitiesDir : CAPABILITIES_DIR;
|
|
341
410
|
const errors = [];
|
|
342
411
|
const capMap = new Map();
|
|
412
|
+
// ADR-2782 D4 — non-fatal diagnostics (e.g. an unknown field inside a reviewer
|
|
413
|
+
// body). These NEVER fail the build; they surface on stderr so a forward-built
|
|
414
|
+
// manifest degrades visibly instead of silently.
|
|
415
|
+
const warnings = [];
|
|
343
416
|
|
|
344
417
|
if (!fs.existsSync(resolvedCapDir)) {
|
|
345
|
-
return { capMap, errors };
|
|
418
|
+
return { capMap, errors, warnings };
|
|
346
419
|
}
|
|
347
420
|
|
|
348
421
|
// Compute wired points ONCE before iterating capabilities so the filesystem
|
|
@@ -366,6 +439,10 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
366
439
|
continue;
|
|
367
440
|
}
|
|
368
441
|
|
|
442
|
+
// Collected BEFORE the error short-circuit below so a manifest that is both
|
|
443
|
+
// forward-built and invalid still reports why it looked unfamiliar.
|
|
444
|
+
for (const w of collectReviewerWarnings(cap)) warnings.push(folderId + '/capability.json: ' + w);
|
|
445
|
+
|
|
369
446
|
const capErrors = validateCapability(cap, folderId);
|
|
370
447
|
if (capErrors.length > 0) {
|
|
371
448
|
for (const e of capErrors) errors.push(folderId + '/capability.json: ' + e);
|
|
@@ -397,7 +474,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
397
474
|
}
|
|
398
475
|
|
|
399
476
|
// Cross-capability invariants — capMap contains only fully-valid capabilities at this point.
|
|
400
|
-
const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys);
|
|
477
|
+
const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys, resolvedCentralPatterns);
|
|
401
478
|
errors.push(...crossErrors);
|
|
402
479
|
|
|
403
480
|
// C2: Global consumes-satisfiability — runs after capMap is fully built so cross-capability
|
|
@@ -406,7 +483,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
406
483
|
const consumesErrors = validateConsumesGlobal(capMap);
|
|
407
484
|
errors.push(...consumesErrors);
|
|
408
485
|
|
|
409
|
-
return { capMap, errors };
|
|
486
|
+
return { capMap, errors, warnings };
|
|
410
487
|
}
|
|
411
488
|
|
|
412
489
|
/**
|
|
@@ -446,6 +523,46 @@ function buildRegistry(capMap) {
|
|
|
446
523
|
if (capId === '__proto__' || capId === 'constructor' || capId === 'prototype') continue;
|
|
447
524
|
capabilities[capId] = cap;
|
|
448
525
|
|
|
526
|
+
// Federated config slice — harvested from ANY role that declares one.
|
|
527
|
+
//
|
|
528
|
+
// ADR-2782 D1/D9: this loop was nested inside the `role === 'feature'` branch,
|
|
529
|
+
// so a `role: "runtime"` capability's `config` was read by nothing and dropped
|
|
530
|
+
// in silence — the actual reason reviewer config keys are stranded in the
|
|
531
|
+
// central schema. (The often-cited reason, that the runtime body forbids
|
|
532
|
+
// feature-only fields, does not apply: `config` is NOT in
|
|
533
|
+
// FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME.) Owning a config slice is a property of
|
|
534
|
+
// DECLARING one, not of being a feature. Verified inert at introduction — no
|
|
535
|
+
// shipped capability declares `config` on a non-feature role — so this changes
|
|
536
|
+
// no existing key; it stops a latent silent drop and unblocks Phase 4 (#2797).
|
|
537
|
+
for (const key of Object.keys(cap.config || {})) {
|
|
538
|
+
// S2b: inline literal guard at each write site (CodeQL barrier)
|
|
539
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
540
|
+
configKeys[key] = capId;
|
|
541
|
+
|
|
542
|
+
// Build configSchema entry — validate the slice first (throw on violation)
|
|
543
|
+
const slice = (cap.config || {})[key];
|
|
544
|
+
const sliceErrors = validateConfigSliceEntry(capId, key, slice);
|
|
545
|
+
if (sliceErrors.length > 0) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
'configSchema validation failed during registry build:\n' +
|
|
548
|
+
sliceErrors.map((e) => ' ' + e).join('\n'),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
// S2b: inline literal guard for configSchema write site
|
|
552
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
553
|
+
configSchema[key] = {
|
|
554
|
+
owner: capId,
|
|
555
|
+
type: slice.type,
|
|
556
|
+
default: slice.default,
|
|
557
|
+
description: slice.description,
|
|
558
|
+
};
|
|
559
|
+
// Preserve values array for enum types if present
|
|
560
|
+
if (slice.type === 'enum' && Array.isArray(slice.values)) {
|
|
561
|
+
configSchema[key].values = slice.values;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
449
566
|
if (cap.role === 'feature') {
|
|
450
567
|
for (const skill of (cap.skills || [])) {
|
|
451
568
|
// S2b: inline literal guard at each write site (CodeQL barrier)
|
|
@@ -457,34 +574,6 @@ function buildRegistry(capMap) {
|
|
|
457
574
|
if (agent === '__proto__' || agent === 'constructor' || agent === 'prototype') continue;
|
|
458
575
|
byAgent[agent] = capId;
|
|
459
576
|
}
|
|
460
|
-
for (const key of Object.keys(cap.config || {})) {
|
|
461
|
-
// S2b: inline literal guard at each write site (CodeQL barrier)
|
|
462
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
463
|
-
configKeys[key] = capId;
|
|
464
|
-
|
|
465
|
-
// Build configSchema entry — validate the slice first (throw on violation)
|
|
466
|
-
const slice = (cap.config || {})[key];
|
|
467
|
-
const sliceErrors = validateConfigSliceEntry(capId, key, slice);
|
|
468
|
-
if (sliceErrors.length > 0) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
'configSchema validation failed during registry build:\n' +
|
|
471
|
-
sliceErrors.map((e) => ' ' + e).join('\n'),
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
// S2b: inline literal guard for configSchema write site
|
|
475
|
-
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
476
|
-
configSchema[key] = {
|
|
477
|
-
owner: capId,
|
|
478
|
-
type: slice.type,
|
|
479
|
-
default: slice.default,
|
|
480
|
-
description: slice.description,
|
|
481
|
-
};
|
|
482
|
-
// Preserve values array for enum types if present
|
|
483
|
-
if (slice.type === 'enum' && Array.isArray(slice.values)) {
|
|
484
|
-
configSchema[key].values = slice.values;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
577
|
|
|
489
578
|
for (const step of (cap.steps || [])) {
|
|
490
579
|
if (VALID_LOOP_POINTS.has(step.point)) {
|
|
@@ -741,7 +830,11 @@ function main() {
|
|
|
741
830
|
if (flag === '--check') {
|
|
742
831
|
// Fix #3: read the REAL central config keys so collision detection fires and is visible.
|
|
743
832
|
const centralKeys = loadCentralConfigKeys();
|
|
744
|
-
const { capMap, errors } = loadAndValidate(centralKeys);
|
|
833
|
+
const { capMap, errors, warnings } = loadAndValidate(centralKeys);
|
|
834
|
+
|
|
835
|
+
// ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
|
|
836
|
+
// exit so a forward-built manifest still explains itself on a failing build.
|
|
837
|
+
for (const w of warnings) process.stderr.write(w + '\n');
|
|
745
838
|
|
|
746
839
|
// Separate pending-migration warnings from hard errors
|
|
747
840
|
const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
|
|
@@ -778,7 +871,11 @@ function main() {
|
|
|
778
871
|
} else if (flag === '--write') {
|
|
779
872
|
// Fix #3: read the REAL central config keys so collision detection fires and is visible.
|
|
780
873
|
const centralKeys = loadCentralConfigKeys();
|
|
781
|
-
const { capMap, errors } = loadAndValidate(centralKeys);
|
|
874
|
+
const { capMap, errors, warnings } = loadAndValidate(centralKeys);
|
|
875
|
+
|
|
876
|
+
// ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
|
|
877
|
+
// exit so a forward-built manifest still explains itself on a failing build.
|
|
878
|
+
for (const w of warnings) process.stderr.write(w + '\n');
|
|
782
879
|
|
|
783
880
|
// Separate pending-migration warnings from hard errors
|
|
784
881
|
const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
|
|
@@ -828,6 +925,7 @@ module.exports = {
|
|
|
828
925
|
validateCrossCapability,
|
|
829
926
|
classifyCrossErrors,
|
|
830
927
|
loadCentralConfigKeys,
|
|
928
|
+
loadCentralConfigPatterns,
|
|
831
929
|
loadAndValidate,
|
|
832
930
|
buildRegistry,
|
|
833
931
|
serializeRegistry,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gen-emitted-baseline.cjs — publishes the emitted-baseline artifact the
|
|
6
|
+
* differential attribution check reads via `resolveBaseline()` (#2724, ADR-2719 §5).
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists
|
|
9
|
+
*
|
|
10
|
+
* Phase 3 (#2723) built `resolveBaseline()` (tests/helpers/emitted-baseline.cjs) and its
|
|
11
|
+
* cache-key staleness discipline, but nothing produced the artifact it reads — the
|
|
12
|
+
* dual-run window instead read the baseline straight off the committed golden fixtures
|
|
13
|
+
* at the base ref (`baselineManifestsAtRef`). Phase 4 deletes those fixtures, so
|
|
14
|
+
* something has to build and publish `{version, sha, manifests, sizes}` for real.
|
|
15
|
+
*
|
|
16
|
+
* ## What it does
|
|
17
|
+
*
|
|
18
|
+
* Builds the emitted manifest set (19 real installer spawns) and the workflow/agent size
|
|
19
|
+
* maps for the tree at `--dir <path>` (default: THIS script's own checkout — i.e.
|
|
20
|
+
* whatever commit is currently checked out where `gen-emitted-baseline.cjs` itself
|
|
21
|
+
* lives). Writes the result to `--out <path>` (default `.gsd-cache/emitted-baseline.json`).
|
|
22
|
+
*
|
|
23
|
+
* `--dir` decouples "which copy of this script runs" from "which tree gets measured"
|
|
24
|
+
* (#2767). The MEASUREMENT SCHEMA — this script, and the `currentManifests`/
|
|
25
|
+
* `currentSizes`/`buildParityManifest` functions it calls — always comes from wherever
|
|
26
|
+
* `gen-emitted-baseline.cjs` itself is being run from (relative `require`s resolve
|
|
27
|
+
* there); only the tree being measured (which `bin/install.js` gets spawned, which
|
|
28
|
+
* `hooks/`/`gsd-core/workflows/`/`agents/` get read) moves to `--dir`. That is what lets
|
|
29
|
+
* a differential apply ONE definition of "the emitted manifest" to two different
|
|
30
|
+
* commits and stay comparable even as that definition evolves — see
|
|
31
|
+
* `tests/helpers/emitted-runtime.cjs`'s `buildBaselineAtRef` for the caller that
|
|
32
|
+
* exercises this for real, and why it must NOT run the `--dir` tree's own copy of this
|
|
33
|
+
* script (which may be an older version, or — for any base ref that predates the PR
|
|
34
|
+
* that added this script at all — may not exist there yet).
|
|
35
|
+
*
|
|
36
|
+
* ## Callers
|
|
37
|
+
*
|
|
38
|
+
* 1. CI's push-to-`next` job runs this straight after `next` advances (no `--dir`,
|
|
39
|
+
* measuring its own checkout), then uploads `.gsd-cache/emitted-baseline.json` as a
|
|
40
|
+
* cache entry keyed on the merge sha (.github/workflows/test.yml,
|
|
41
|
+
* `publish-emitted-baseline` job).
|
|
42
|
+
* 2. `tests/emitted-attribution.test.cjs`'s real-tree test passes
|
|
43
|
+
* `buildBaselineAtRef` (tests/helpers/emitted-runtime.cjs) to `resolveBaseline()`
|
|
44
|
+
* as the `buildFallback` for a cache miss: it checks out `base` into a throwaway
|
|
45
|
+
* `git worktree`, symlinks in `node_modules` and runs `npm run build:lib` there
|
|
46
|
+
* (this script and the test helpers are Node-builtins-only per CONTRIBUTING.md's
|
|
47
|
+
* "No external dependencies in core", but `tests/helpers/install-shared.cjs`
|
|
48
|
+
* requires the TSC-compiled, gitignored `gsd-core/bin/lib/*.cjs`, so that one build
|
|
49
|
+
* step is unavoidable), then spawns THIS repo's OWN `gen-emitted-baseline.cjs`
|
|
50
|
+
* with `--dir <worktree> --out <tmp>` and reads the artifact back.
|
|
51
|
+
*
|
|
52
|
+
* Every git subprocess is bounded (CLAUDE.md → KNOWN DEFECTS: unbounded subprocesses
|
|
53
|
+
* hang CI silently).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const fs = require('node:fs');
|
|
57
|
+
const path = require('node:path');
|
|
58
|
+
const { execFileSync } = require('node:child_process');
|
|
59
|
+
|
|
60
|
+
const { runMain, ExitError } = require('./lib/cli-exit.cjs');
|
|
61
|
+
const { BUILD_SCRIPT } = require('../tests/helpers/install-shared.cjs');
|
|
62
|
+
const { currentManifests, currentSizes, git } = require('../tests/helpers/emitted-runtime.cjs');
|
|
63
|
+
const { BASELINE_VERSION } = require('../tests/helpers/emitted-baseline.cjs');
|
|
64
|
+
|
|
65
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
66
|
+
const DEFAULT_OUT = path.join(REPO_ROOT, '.gsd-cache', 'emitted-baseline.json');
|
|
67
|
+
|
|
68
|
+
// Reuses emitted-runtime.cjs's `git()` (not a local execFileSync) so the
|
|
69
|
+
// `-c safe.directory=<dir>` fix for the remote runner's dubious-ownership
|
|
70
|
+
// mount (#2767) has exactly one source of truth — see that module's
|
|
71
|
+
// `safeDirArgs` doc comment.
|
|
72
|
+
function resolveHeadSha(cwd) {
|
|
73
|
+
return git(['rev-parse', 'HEAD'], { cwd }).trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseArgs(argv) {
|
|
77
|
+
let out = DEFAULT_OUT;
|
|
78
|
+
let dir = REPO_ROOT;
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
if (argv[i] === '--out') {
|
|
81
|
+
out = argv[i + 1];
|
|
82
|
+
i++;
|
|
83
|
+
} else if (argv[i] === '--dir') {
|
|
84
|
+
dir = argv[i + 1];
|
|
85
|
+
i++;
|
|
86
|
+
} else {
|
|
87
|
+
throw new ExitError(2, `gen-emitted-baseline: unknown argument "${argv[i]}"`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!out || typeof out !== 'string') {
|
|
91
|
+
throw new ExitError(2, 'gen-emitted-baseline: --out requires a path');
|
|
92
|
+
}
|
|
93
|
+
if (!dir || typeof dir !== 'string') {
|
|
94
|
+
throw new ExitError(2, 'gen-emitted-baseline: --dir requires a path');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
out: path.isAbsolute(out) ? out : path.join(process.cwd(), out),
|
|
98
|
+
dir: path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const { out, dir } = parseArgs(process.argv.slice(2));
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(dir)) {
|
|
106
|
+
throw new ExitError(1, `gen-emitted-baseline: --dir "${dir}" does not exist`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sha = resolveHeadSha(dir);
|
|
110
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) {
|
|
111
|
+
throw new ExitError(1, `gen-emitted-baseline: HEAD of "${dir}" did not resolve to a 40-hex sha: ${sha}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// hooks/dist is gitignored and built; a scoped CI checkout may not have run
|
|
115
|
+
// build:hooks yet. Idempotent, mirrors the real-tree test. Built INSIDE `dir` — the
|
|
116
|
+
// MEASURED tree's own hooks/ source, not this script's — so a `--dir` pointed at a
|
|
117
|
+
// different checkout (e.g. a base-ref worktree) reflects that checkout's hooks, not
|
|
118
|
+
// the caller's. `dir === REPO_ROOT` (the default, no `--dir`) collapses to the
|
|
119
|
+
// pre-#2767 behavior of building this own repo's hooks/dist.
|
|
120
|
+
const buildScript = dir === REPO_ROOT ? BUILD_SCRIPT : path.join(dir, 'scripts', 'build-hooks.js');
|
|
121
|
+
execFileSync(process.execPath, [buildScript], { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 120_000 });
|
|
122
|
+
|
|
123
|
+
// The measurement SCHEMA (currentManifests/currentSizes/buildParityManifest) is
|
|
124
|
+
// always THIS script's own — only the tree being measured moves with `repoRoot`.
|
|
125
|
+
const manifests = currentManifests({ repoRoot: dir });
|
|
126
|
+
const sizes = currentSizes({ repoRoot: dir });
|
|
127
|
+
|
|
128
|
+
const artifact = { version: BASELINE_VERSION, sha, manifests, sizes };
|
|
129
|
+
|
|
130
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
131
|
+
fs.writeFileSync(out, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
132
|
+
|
|
133
|
+
const familyCount = Object.keys(manifests).length;
|
|
134
|
+
const sizeCount = Object.keys(sizes).length;
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`gen-emitted-baseline: wrote ${out} (dir=${dir}, sha=${sha.slice(0, 12)}, ${familyCount} families, ${sizeCount} sized files)\n`,
|
|
137
|
+
);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (require.main === module) {
|
|
142
|
+
runMain(main);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { parseArgs, resolveHeadSha, DEFAULT_OUT, REPO_ROOT };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regenerate the per-file test timing table used to weight chunk packing in
|
|
3
|
+
// scripts/run-tests.cjs (issue #2456).
|
|
4
|
+
//
|
|
5
|
+
// The chunk packer needs to know what each test file actually COSTS. Before
|
|
6
|
+
// #2456 it guessed from the filename (`^(?:install|codex-)` scored 12x,
|
|
7
|
+
// everything else 1) and was wrong in both directions — installer-migration-
|
|
8
|
+
// authoring.test.cjs scored 12x but runs ~0.1s, while the two heaviest files in
|
|
9
|
+
// the suite (run-tests-harness.test.cjs and release-tarball-smoke.install
|
|
10
|
+
// .test.cjs) both scored 1. This script replaces the guess with measurement.
|
|
11
|
+
//
|
|
12
|
+
// Input is one or more node:test reporter event streams as emitted by
|
|
13
|
+
// `gsd-test` (`~/.local/state/gsd-test/runs/<run-id>/test-events-<os>-node<v>
|
|
14
|
+
// .jsonl`). Each stream carries one `test:summary` event per test FILE, whose
|
|
15
|
+
// `data.duration_ms` is that file's total wall-clock and whose `data.file` is
|
|
16
|
+
// its absolute in-container path.
|
|
17
|
+
//
|
|
18
|
+
// Usage:
|
|
19
|
+
// node scripts/gen-test-timings.cjs <events.jsonl> [<events.jsonl> ...]
|
|
20
|
+
// node scripts/gen-test-timings.cjs ~/.local/state/gsd-test/runs/*/test-events-*.jsonl
|
|
21
|
+
// node scripts/gen-test-timings.cjs events.jsonl --out tests/test-timings.json
|
|
22
|
+
//
|
|
23
|
+
// When several streams are supplied (multiple lanes, e.g. node22 + node24), a
|
|
24
|
+
// file's recorded time is the MAX across them, not the mean: the packer exists
|
|
25
|
+
// to keep the SLOWEST lane's slowest chunk away from the per-chunk timeout, so
|
|
26
|
+
// the conservative bound is the right one to balance against.
|
|
27
|
+
//
|
|
28
|
+
// The table is ADVISORY and deliberately un-gated — there is no `--check` mode
|
|
29
|
+
// and no CI lint that fails on staleness, because timing data legitimately
|
|
30
|
+
// varies run to run. A file missing from the table falls back to the table's
|
|
31
|
+
// median weight, so a stale table degrades chunk BALANCE gracefully instead of
|
|
32
|
+
// failing the build. Regenerate it when the suite's cost profile has visibly
|
|
33
|
+
// drifted, not on a schedule.
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const { basename, dirname, join } = require('path');
|
|
38
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
39
|
+
|
|
40
|
+
const DEFAULT_OUT = join(__dirname, '..', 'tests', 'test-timings.json');
|
|
41
|
+
const SCHEMA_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const inputs = [];
|
|
45
|
+
let out = DEFAULT_OUT;
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
if (arg === '--out') {
|
|
49
|
+
const value = argv[++i];
|
|
50
|
+
if (!value) return { error: '--out requires a path' };
|
|
51
|
+
out = value;
|
|
52
|
+
} else if (arg.startsWith('--out=')) {
|
|
53
|
+
const value = arg.slice('--out='.length);
|
|
54
|
+
if (!value) return { error: '--out requires a path' };
|
|
55
|
+
out = value;
|
|
56
|
+
} else if (arg.startsWith('-')) {
|
|
57
|
+
return { error: `unknown flag "${arg}"` };
|
|
58
|
+
} else {
|
|
59
|
+
inputs.push(arg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (inputs.length === 0) {
|
|
63
|
+
return { error: 'usage: gen-test-timings.cjs <reporter-events.jsonl> [...] [--out <path>]' };
|
|
64
|
+
}
|
|
65
|
+
return { inputs, out };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fold one reporter event stream into `acc`, keeping the MAX duration seen for
|
|
69
|
+
// each test file. Returns per-stream counters plus any basename collisions
|
|
70
|
+
// found WITHIN this stream.
|
|
71
|
+
//
|
|
72
|
+
// Keying is by BASENAME, matching how run-tests.cjs weights a selected file:
|
|
73
|
+
// the reporter reports absolute in-container paths (/work/tests/foo.test.cjs)
|
|
74
|
+
// while the harness carries paths relative to its test dir, so the basename is
|
|
75
|
+
// the only stable join key between the two. A basename seen in two different
|
|
76
|
+
// directories would make the table ambiguous, so it must fail loudly.
|
|
77
|
+
//
|
|
78
|
+
// Collision detection is scoped to a SINGLE stream deliberately. Every lane
|
|
79
|
+
// writes its own stream under its own container root (`/work/tests` on Linux,
|
|
80
|
+
// `C:/work/tests` on Windows), so comparing directories ACROSS streams reports
|
|
81
|
+
// every shared basename as a collision — which is the script's own documented
|
|
82
|
+
// usage (globbing `test-events-*.jsonl` across lanes). Within one stream the
|
|
83
|
+
// root is constant, so a differing directory is a real collision.
|
|
84
|
+
function foldStream(text, acc) {
|
|
85
|
+
const dirsByBase = new Map();
|
|
86
|
+
let files = 0;
|
|
87
|
+
let malformed = 0;
|
|
88
|
+
for (const line of text.split('\n')) {
|
|
89
|
+
if (line.trim() === '') continue;
|
|
90
|
+
let event;
|
|
91
|
+
try {
|
|
92
|
+
event = JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
malformed++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!event || event.type !== 'test:summary') continue;
|
|
98
|
+
const data = event.data;
|
|
99
|
+
if (!data || typeof data.file !== 'string') continue;
|
|
100
|
+
const ms = data.duration_ms;
|
|
101
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) continue;
|
|
102
|
+
const path = data.file.replace(/\\/g, '/');
|
|
103
|
+
const base = basename(path);
|
|
104
|
+
if (!dirsByBase.has(base)) dirsByBase.set(base, new Set());
|
|
105
|
+
dirsByBase.get(base).add(dirname(path));
|
|
106
|
+
const prev = acc.get(base);
|
|
107
|
+
if (prev === undefined || ms > prev) acc.set(base, ms);
|
|
108
|
+
files++;
|
|
109
|
+
}
|
|
110
|
+
const collisions = [...dirsByBase.entries()]
|
|
111
|
+
.filter(([, dirs]) => dirs.size > 1)
|
|
112
|
+
.map(([base, dirs]) => `${base} (${[...dirs].sort().join(', ')})`);
|
|
113
|
+
return { files, malformed, collisions };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
118
|
+
if (parsed.error) throw new ExitError(2, `gen-test-timings: ${parsed.error}`);
|
|
119
|
+
|
|
120
|
+
const acc = new Map();
|
|
121
|
+
const allCollisions = new Set();
|
|
122
|
+
const sources = [];
|
|
123
|
+
for (const input of parsed.inputs) {
|
|
124
|
+
let text;
|
|
125
|
+
try {
|
|
126
|
+
text = fs.readFileSync(input, 'utf8');
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new ExitError(2, `gen-test-timings: cannot read "${input}": ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
const { files, malformed, collisions } = foldStream(text, acc);
|
|
131
|
+
for (const c of collisions) allCollisions.add(c);
|
|
132
|
+
sources.push(basename(input));
|
|
133
|
+
console.error(
|
|
134
|
+
`gen-test-timings: ${basename(input)} — ${files} file summaries` +
|
|
135
|
+
(malformed > 0 ? `, ${malformed} unparseable lines skipped` : ''),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (acc.size === 0) {
|
|
140
|
+
throw new ExitError(
|
|
141
|
+
2,
|
|
142
|
+
'gen-test-timings: no `test:summary` events with a file and duration_ms were found. ' +
|
|
143
|
+
'Check that the input is a node:test reporter event stream (test-events-<os>-node<v>.jsonl).',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A basename that resolves to two different directories within one lane makes
|
|
148
|
+
// the table ambiguous: run-tests.cjs joins on basename alone, so one file's
|
|
149
|
+
// measured cost would silently be applied to the other. Fail rather than emit
|
|
150
|
+
// a table that lies.
|
|
151
|
+
if (allCollisions.size > 0) {
|
|
152
|
+
throw new ExitError(
|
|
153
|
+
2,
|
|
154
|
+
`gen-test-timings: basename collision — the table cannot key on basename alone:\n ${[...allCollisions].sort().join('\n ')}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Every key must be a plain test-file basename. This is a data-integrity
|
|
159
|
+
// check on a stream we do not control (the reporter emits whatever path the
|
|
160
|
+
// runner saw), and it structurally excludes a computed key like `__proto__`
|
|
161
|
+
// or `constructor` from being written into the table object below — the
|
|
162
|
+
// `js/prototype-polluting-assignment` shape, even though the value here is
|
|
163
|
+
// always a number and could not actually pollute.
|
|
164
|
+
const SAFE_BASENAME_RE = /^[A-Za-z0-9._-]+\.test\.cjs$/;
|
|
165
|
+
const rejected = [...acc.keys()].filter((base) => !SAFE_BASENAME_RE.test(base));
|
|
166
|
+
if (rejected.length > 0) {
|
|
167
|
+
throw new ExitError(
|
|
168
|
+
2,
|
|
169
|
+
`gen-test-timings: refusing to emit non-test-file keys: ${rejected.sort().join(', ')}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Sorted keys keep the checked-in diff reviewable: a regeneration shows only
|
|
174
|
+
// the files whose cost actually moved, not a reshuffled object.
|
|
175
|
+
const timings = Object.create(null);
|
|
176
|
+
for (const base of [...acc.keys()].sort()) {
|
|
177
|
+
timings[base] = Math.round(acc.get(base));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const payload = {
|
|
181
|
+
schema_version: SCHEMA_VERSION,
|
|
182
|
+
generated_by: 'scripts/gen-test-timings.cjs',
|
|
183
|
+
unit: 'ms',
|
|
184
|
+
sources: sources.sort(),
|
|
185
|
+
file_count: acc.size,
|
|
186
|
+
timings,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
fs.writeFileSync(parsed.out, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
190
|
+
const totalMs = [...acc.values()].reduce((sum, ms) => sum + ms, 0);
|
|
191
|
+
console.error(
|
|
192
|
+
`gen-test-timings: wrote ${parsed.out} — ${acc.size} files, ${(totalMs / 1000).toFixed(1)}s total`,
|
|
193
|
+
);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (require.main === module) {
|
|
198
|
+
runMain(main);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { parseArgs, foldStream };
|