@bongos/core 1.18.18 → 1.19.2
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/.bongos-core.json +515 -195
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +99 -14
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -1
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +124 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Goal board CLI — list goals, or show one goal's criteria + tasks + what to claim next.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]
|
|
6
|
+
// node scripts/gds/goal.js show <id>
|
|
7
|
+
//
|
|
8
|
+
// Read-only. Default list filter is --status open.
|
|
9
|
+
|
|
10
|
+
const { cliClient, requireSession, arg } = require('./cli-lib');
|
|
11
|
+
|
|
12
|
+
const GOAL_GLYPH = { open: '○', achieved: '✓', archived: '·' };
|
|
13
|
+
const IN_FLIGHT = new Set(['active', 'completed', 'confirmed']);
|
|
14
|
+
const LIST_STATUSES = ['open', 'achieved', 'archived', 'all'];
|
|
15
|
+
|
|
16
|
+
function usageExit() {
|
|
17
|
+
console.error('usage: node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]');
|
|
18
|
+
console.error(' node scripts/gds/goal.js show <id>');
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// abandoned is a terminal state (deliberately cut), not pending work — excluded
|
|
23
|
+
// from every denominator, the same rule migration 017 applies to version_progress.
|
|
24
|
+
function liveTasks(tasks) {
|
|
25
|
+
return tasks.filter((t) => t.status !== 'abandoned');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rollup(tasks) {
|
|
29
|
+
const live = liveTasks(tasks);
|
|
30
|
+
const shipped = live.filter((t) => t.status === 'shipped').length;
|
|
31
|
+
const ready = live.filter((t) => t.status === 'ready').length;
|
|
32
|
+
const inFlight = live.filter((t) => IN_FLIGHT.has(t.status)).length;
|
|
33
|
+
const parts = [`${shipped}/${live.length} shipped`];
|
|
34
|
+
if (inFlight) parts.push(`${inFlight} in flight`);
|
|
35
|
+
if (ready) parts.push(`${ready} ready`);
|
|
36
|
+
return parts.join(' · ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function list(api, args) {
|
|
40
|
+
const version = arg('--version', args) || null;
|
|
41
|
+
if (args.includes('--version') && (!version || !/^[\w.-]+$/.test(version))) usageExit();
|
|
42
|
+
const status = arg('--status', args) || 'open';
|
|
43
|
+
if (!LIST_STATUSES.includes(status)) usageExit();
|
|
44
|
+
const query = {};
|
|
45
|
+
if (version) query.version = version;
|
|
46
|
+
if (status !== 'all') query.status = status;
|
|
47
|
+
|
|
48
|
+
const g = await api.goals.getGoals({ query });
|
|
49
|
+
if (!g.ok) {
|
|
50
|
+
console.error('goals list failed:', JSON.stringify(g.data));
|
|
51
|
+
// process.exitCode (not process.exit): hard-exit while undici keep-alive
|
|
52
|
+
// sockets tear down trips a libuv assertion on Windows.
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const goals = g.data.goals || [];
|
|
57
|
+
if (goals.length === 0) {
|
|
58
|
+
console.log('No goals match.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// One goal_id-filtered fetch per listed goal, in parallel — returns exactly
|
|
63
|
+
// the rows the rollups need, never a version-wide or table-wide over-fetch.
|
|
64
|
+
const results = await Promise.all(goals.map((goal) => api.tasks.getTasks({ query: { goal_id: goal.id } })));
|
|
65
|
+
const failed = results.find((r) => !r.ok);
|
|
66
|
+
if (failed) {
|
|
67
|
+
console.error('tasks fetch failed (rollups would be wrong):', JSON.stringify(failed.data));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const byGoal = new Map(goals.map((goal, i) => [String(goal.id), results[i].data.tasks || []]));
|
|
72
|
+
|
|
73
|
+
for (const goal of goals) {
|
|
74
|
+
const glyph = GOAL_GLYPH[goal.status] || '?';
|
|
75
|
+
const tasks = byGoal.get(String(goal.id)) || [];
|
|
76
|
+
const tail = liveTasks(tasks).length ? ` [${rollup(tasks)}]` : '';
|
|
77
|
+
console.log(`${glyph} #${goal.id} ${goal.version_id} ${goal.title}${tail}`);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(`${goals.length} goal${goals.length === 1 ? '' : 's'} · node scripts/gds/goal.js show <id> for detail`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function criterionGlyph(c) {
|
|
84
|
+
if (c.satisfied) return '✓';
|
|
85
|
+
if (c.pending_review) return '◇';
|
|
86
|
+
return '○';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function criterionState(c) {
|
|
90
|
+
if (c.satisfied) return 'satisfied';
|
|
91
|
+
if (c.pending_review) return 'met — pending review (/goal-review)';
|
|
92
|
+
const linked = Number(c.linked_tasks) || 0;
|
|
93
|
+
if (linked === 0) return 'no linked tasks yet';
|
|
94
|
+
const n = Number(c.unshipped_tasks) || 0;
|
|
95
|
+
return `${n} of ${linked} linked task${linked === 1 ? '' : 's'} unshipped`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function show(api, id) {
|
|
99
|
+
const [g, t] = await Promise.all([
|
|
100
|
+
api.goals.getGoalsId({ id }),
|
|
101
|
+
api.tasks.getTasks({ query: { goal_id: id } }),
|
|
102
|
+
]);
|
|
103
|
+
if (!g.ok) {
|
|
104
|
+
console.error(`goal ${id} fetch failed:`, JSON.stringify(g.data));
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!t.ok) {
|
|
109
|
+
console.error(`tasks fetch for goal ${id} failed:`, JSON.stringify(t.data));
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { goal, members, criteria, dependencies } = g.data;
|
|
114
|
+
const tasks = t.data.tasks || [];
|
|
115
|
+
|
|
116
|
+
console.log(`#${goal.id} ${goal.title}`);
|
|
117
|
+
console.log(`${goal.version_id} · ${goal.status} · modules: ${(goal.scope_modules || []).join(', ') || '—'}`);
|
|
118
|
+
const allMembers = members || [];
|
|
119
|
+
const lead = allMembers.filter((m) => m.role === 'lead').map((m) => `@${m.github_login}`);
|
|
120
|
+
if (lead.length) {
|
|
121
|
+
console.log(`lead: ${lead.join(', ')}${allMembers.length > lead.length ? ` (+${allMembers.length - lead.length} members)` : ''}`);
|
|
122
|
+
} else if (allMembers.length) {
|
|
123
|
+
console.log(`members: ${allMembers.length} (no lead)`);
|
|
124
|
+
}
|
|
125
|
+
for (const d of dependencies || []) {
|
|
126
|
+
console.log(`prerequisite goal: #${d.id ?? d.goal_id} ${d.title || ''} ${d.satisfied ? '(satisfied)' : '(NOT satisfied)'}`);
|
|
127
|
+
}
|
|
128
|
+
if (goal.description) {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(goal.description);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ((criteria || []).length) {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log('Done-when criteria:');
|
|
136
|
+
for (const c of criteria) {
|
|
137
|
+
console.log(` ${criterionGlyph(c)} ${c.criterion_id} — ${criterionState(c)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (tasks.length === 0) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log('No tasks filed under this goal yet.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await printTaskBuckets(api, tasks);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pure bucketing, split from printing so it unit-tests without a live API.
|
|
150
|
+
function bucketTasks(tasks) {
|
|
151
|
+
return {
|
|
152
|
+
shipped: tasks.filter((x) => x.status === 'shipped'),
|
|
153
|
+
inFlight: tasks.filter((x) => IN_FLIGHT.has(x.status)),
|
|
154
|
+
ready: tasks.filter((x) => x.status === 'ready'),
|
|
155
|
+
gated: tasks.filter((x) => x.status === 'backlog' || x.status === 'blocked'),
|
|
156
|
+
abandoned: tasks.filter((x) => x.status === 'abandoned'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function printTaskBuckets(api, tasks) {
|
|
161
|
+
const buckets = bucketTasks(tasks);
|
|
162
|
+
const live = liveTasks(tasks);
|
|
163
|
+
|
|
164
|
+
// Explain WHY each gated task waits — parallel, with per-task catch: one
|
|
165
|
+
// failed dependency fetch degrades to "dependencies unknown" for that task
|
|
166
|
+
// instead of aborting the listing.
|
|
167
|
+
const waits = new Map();
|
|
168
|
+
const waitsUnknown = new Set();
|
|
169
|
+
await Promise.all(buckets.gated.map(async (task) => {
|
|
170
|
+
try {
|
|
171
|
+
const d = await api.tasks.getTasksIdDependencies({ id: task.id });
|
|
172
|
+
if (!d.ok) { waitsUnknown.add(String(task.id)); return; }
|
|
173
|
+
const unshipped = (d.data.dependencies || []).filter((dep) => dep.status !== 'shipped');
|
|
174
|
+
if (unshipped.length) waits.set(String(task.id), unshipped.map((dep) => `task ${dep.id} (${dep.status})`).join(', '));
|
|
175
|
+
} catch {
|
|
176
|
+
waitsUnknown.add(String(task.id));
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
console.log('');
|
|
181
|
+
const abandonedNote = buckets.abandoned.length ? `, ${buckets.abandoned.length} abandoned` : '';
|
|
182
|
+
console.log(`Tasks (${buckets.shipped.length}/${live.length} shipped${abandonedNote}):`);
|
|
183
|
+
for (const x of buckets.shipped) console.log(` ✓ ${x.id} ${x.title}`);
|
|
184
|
+
for (const x of buckets.inFlight) console.log(` ▶ ${x.id} ${x.title} [${x.status}]`);
|
|
185
|
+
for (const x of buckets.ready) console.log(` ● ${x.id} ${x.title} [ready — claimable now]`);
|
|
186
|
+
for (const x of buckets.gated) {
|
|
187
|
+
const key = String(x.id);
|
|
188
|
+
const why = waits.has(key) ? ` — waits on ${waits.get(key)}`
|
|
189
|
+
: waitsUnknown.has(key) ? ' — dependencies unknown (fetch failed)' : '';
|
|
190
|
+
console.log(` ○ ${x.id} ${x.title} [${x.status}${why}]`);
|
|
191
|
+
}
|
|
192
|
+
for (const x of buckets.abandoned) console.log(` ✕ ${x.id} ${x.title} [abandoned — excluded from progress]`);
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
if (buckets.ready.length) {
|
|
196
|
+
console.log(`Next: node scripts/gds/claim.js ${buckets.ready[0].id}`);
|
|
197
|
+
} else if (buckets.inFlight.length) {
|
|
198
|
+
console.log('Nothing claimable — work in flight must land first.');
|
|
199
|
+
} else if (buckets.gated.length) {
|
|
200
|
+
const allWait = buckets.gated.every((x) => waits.has(String(x.id)));
|
|
201
|
+
console.log(allWait
|
|
202
|
+
? 'Nothing claimable — every remaining task waits on an unshipped dependency.'
|
|
203
|
+
: 'Nothing claimable — remaining tasks are backlog/blocked and not yet promoted to ready.');
|
|
204
|
+
} else if (live.length === 0) {
|
|
205
|
+
console.log('No live tasks — everything filed here was abandoned.');
|
|
206
|
+
} else {
|
|
207
|
+
console.log('All tasks shipped — run /goal-review to confirm criteria.');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function main() {
|
|
212
|
+
await requireSession();
|
|
213
|
+
const args = process.argv.slice(2);
|
|
214
|
+
const cmd = args[0];
|
|
215
|
+
const api = await cliClient();
|
|
216
|
+
|
|
217
|
+
if (cmd === 'list') {
|
|
218
|
+
await list(api, args.slice(1));
|
|
219
|
+
} else if (cmd === 'show') {
|
|
220
|
+
if (!/^\d+$/.test(args[1] || '')) usageExit();
|
|
221
|
+
await show(api, Number(args[1]));
|
|
222
|
+
} else {
|
|
223
|
+
usageExit();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (require.main === module) {
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
console.error('goal.js failed:', err && err.message ? err.message : err);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { rollup, criterionGlyph, criterionState, bucketTasks, liveTasks };
|
package/scripts/gds/init.js
CHANGED
|
@@ -364,7 +364,11 @@ function kickoffTasks(spec) {
|
|
|
364
364
|
if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
|
|
365
365
|
tasks.push({
|
|
366
366
|
title: 'Fill the instance look with your own LLM',
|
|
367
|
-
|
|
367
|
+
// 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
|
|
368
|
+
// kind and the task silently vanished from the checklist (the one non-infra
|
|
369
|
+
// kickoff step). Deciding the instance look is a decision by an ideator.
|
|
370
|
+
kind: 'decision',
|
|
371
|
+
discipline: 'ideator',
|
|
368
372
|
description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
|
|
369
373
|
'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
|
|
370
374
|
});
|
|
@@ -451,6 +455,9 @@ function versionPayload(spec) {
|
|
|
451
455
|
id: v.id,
|
|
452
456
|
name: v.name,
|
|
453
457
|
status: v.status || 'building',
|
|
458
|
+
// versions.track is NOT NULL (no DB default) — always send it explicitly.
|
|
459
|
+
// An instance's first version is its product track unless the spec says otherwise.
|
|
460
|
+
track: v.track === 'internal' ? 'internal' : 'product',
|
|
454
461
|
done_when: v.done_when,
|
|
455
462
|
criteria: [{ criterion_id: v.criterion_id ? slugify(v.criterion_id) : 'first-goal', criterion_md: v.done_when }],
|
|
456
463
|
};
|
|
@@ -728,12 +735,19 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
|
|
|
728
735
|
else log(` ✓ created version ${vp.id} + 1 done-when goal`);
|
|
729
736
|
const criterionId = vp.criteria[0].criterion_id;
|
|
730
737
|
|
|
731
|
-
// A tracked kickoff/starter task (status ready
|
|
738
|
+
// A tracked kickoff/starter task (status ready). Returns true on success.
|
|
739
|
+
// The whole point of filing these is "nothing is silently skipped" — so a
|
|
740
|
+
// rejected POST must be LOUD: every failure is collected with its reason and
|
|
741
|
+
// reported against the expected count, never absorbed into a smaller total.
|
|
742
|
+
let attempted = 0;
|
|
743
|
+
const failures = [];
|
|
732
744
|
const postTask = async (t, extra = {}) => {
|
|
745
|
+
attempted++;
|
|
733
746
|
const res = await api('POST', '/api/gds/tasks', {
|
|
734
747
|
version_id: vp.id, title: t.title, description: t.description || '', status: 'ready',
|
|
735
|
-
kind: t.kind || 'infra', discipline: 'engineer', source: 'bongos-init', ...extra,
|
|
748
|
+
kind: t.kind || 'infra', discipline: t.discipline || 'engineer', source: 'bongos-init', ...extra,
|
|
736
749
|
});
|
|
750
|
+
if (!res.ok) failures.push({ title: t.title, status: res.status, detail: JSON.stringify(res.data) });
|
|
737
751
|
return res.ok;
|
|
738
752
|
};
|
|
739
753
|
|
|
@@ -771,8 +785,12 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
|
|
|
771
785
|
}
|
|
772
786
|
|
|
773
787
|
const kickoffLabel = provisioned ? `${kickoff.length} kickoff + provisioning request` : `${kickoff.length} kickoff`;
|
|
774
|
-
|
|
775
|
-
|
|
788
|
+
if (failures.length) {
|
|
789
|
+
log(` ✗ ${failures.length} of ${attempted} task(s) FAILED to file — the checklist below is INCOMPLETE:`);
|
|
790
|
+
for (const f of failures) log(` ✗ "${f.title}" (${f.status}): ${f.detail}`);
|
|
791
|
+
}
|
|
792
|
+
log(` ${failures.length ? '⚠' : '✓'} filed ${created} of ${attempted} task(s) (${starters.length} starter + ${kickoffLabel})`);
|
|
793
|
+
return { seeded: true, created, attempted, failures, provisioned };
|
|
776
794
|
}
|
|
777
795
|
|
|
778
796
|
// ---------------------------------------------------------------------------
|
|
@@ -795,7 +813,12 @@ function confirmProceed({ input = process.stdin, output = process.stdout } = {})
|
|
|
795
813
|
// seeds the repo's REAL version/goal + IMPORTS its existing backlog (task 2044), NOT a
|
|
796
814
|
// synthetic "V1 — Prove the loop". `--dry-run` previews everything and writes nothing;
|
|
797
815
|
// `--no-seed` layers without seeding. Returns a process exit code.
|
|
798
|
-
|
|
816
|
+
// `coreDep`/`vendorPlan` (task 1002562): adopt used to ALWAYS write the caret range,
|
|
817
|
+
// which resolves NOWHERE on npm — so an adopted repo could not `npm ci --omit=dev`, the
|
|
818
|
+
// exact command the provisioner runs on the box. Greenfield already had --vendor-core for
|
|
819
|
+
// this; passing the same resolved pin through here makes an adopted repo equally
|
|
820
|
+
// installable. Omitted (a plain `bongos init --adopt` on a laptop) → the caret, unchanged.
|
|
821
|
+
async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, coreDep = null, vendorPlan = null, log = console.log }) {
|
|
799
822
|
log(`\nAdopting Cloud Bongos onto the existing repo at "${dir}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
|
|
800
823
|
log(' Layering — pre-existing files are left untouched; nothing is overwritten.');
|
|
801
824
|
|
|
@@ -827,7 +850,30 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
|
|
|
827
850
|
}
|
|
828
851
|
|
|
829
852
|
const cfg = layerConfigs(spec, { dir, dryRun, log });
|
|
830
|
-
const pkg = layerPackageJson(spec, { dir, dryRun, log });
|
|
853
|
+
const pkg = layerPackageJson(spec, { dir, dryRun, log, ...(coreDep ? { coreRange: coreDep } : {}) });
|
|
854
|
+
|
|
855
|
+
// Vendor the pinned core tarball + generate the lockfile, mirroring the greenfield
|
|
856
|
+
// --vendor-core steps, so the adopted repo is `npm ci --omit=dev`-installable (ADR 0108).
|
|
857
|
+
// Skipped when layerPackageJson found the dep ALREADY present — re-pinning someone's
|
|
858
|
+
// existing declaration is an overwrite, and adopt never overwrites.
|
|
859
|
+
if (vendorPlan && pkg.action !== 'present' && pkg.action !== 'unparseable') {
|
|
860
|
+
const upgrade = require('./upgrade');
|
|
861
|
+
if (dryRun) {
|
|
862
|
+
log(`\n [dry-run] would vendor ${vendorPlan.tgz} → ${vendorPlan.vendorRel} and run \`npm install\` to generate package-lock.json`);
|
|
863
|
+
} else {
|
|
864
|
+
log('\nVendoring the pinned @bongos/core + generating the lockfile:');
|
|
865
|
+
upgrade.vendorTarball({ instanceDir: dir, fromTgz: vendorPlan.tgz, targetVersion: vendorPlan.version });
|
|
866
|
+
log(` ✓ vendored ${vendorPlan.vendorRel}`);
|
|
867
|
+
const inst = upgrade.npmInstall(dir);
|
|
868
|
+
if (!inst.ok) {
|
|
869
|
+
log(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated; run \`npm install\` in ${dir} by hand before provisioning.`);
|
|
870
|
+
} else {
|
|
871
|
+
log(' ✓ npm install → package-lock.json generated (node_modules/@bongos/core installed)');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
} else if (vendorPlan && pkg.action === 'present') {
|
|
875
|
+
log(` • ${CORE_PKG} was already declared — left as-is; the vendored tarball was NOT pinned over it.`);
|
|
876
|
+
}
|
|
831
877
|
|
|
832
878
|
log('\nMaterializing the .claude/ methodology surface (additive — existing files left untouched):');
|
|
833
879
|
const claude = materializeClaude({ coreRoot: resolveCoreRoot(), instanceDir: dir, dryRun, overwrite: false, log });
|
|
@@ -846,7 +892,10 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
|
|
|
846
892
|
? `${backlog.tasks.length} imported task(s) from ${backlog.source}${backlog.truncated ? ` (capped at ${BACKLOG_MAX})` : ''}`
|
|
847
893
|
: 'no backlog found (capture tasks later)';
|
|
848
894
|
log(`\nReflecting real state — seeding version "${spec.version && spec.version.id}" + goal + ${importNote}:`);
|
|
849
|
-
await seedViaApi(spec, { dryRun, log });
|
|
895
|
+
const seedResult = (await seedViaApi(spec, { dryRun, log })) || {};
|
|
896
|
+
if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
|
|
897
|
+
log(`\n ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
|
|
898
|
+
}
|
|
850
899
|
} else {
|
|
851
900
|
log('\n (--no-seed: layered config only; skipped real-state seeding)');
|
|
852
901
|
}
|
|
@@ -907,17 +956,16 @@ async function main(argv) {
|
|
|
907
956
|
return 1;
|
|
908
957
|
}
|
|
909
958
|
|
|
910
|
-
//
|
|
911
|
-
// scaffold/overwrite below. Its own report + next-steps live in runAdopt.
|
|
912
|
-
if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept });
|
|
913
|
-
|
|
914
|
-
// Greenfield core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
|
|
959
|
+
// Core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
|
|
915
960
|
// package.json to the vendored `file:` tarball (the caret range resolves NOWHERE on npm,
|
|
916
961
|
// so a caret-only scaffold can't `npm ci`); the tarball is copied + the lockfile generated
|
|
917
|
-
//
|
|
962
|
+
// below. Without it, the scaffold carries the declared caret (a local/dev scaffold the
|
|
918
963
|
// owner resolves another way). Reuses the bongos-upgrade vendoring + install steps.
|
|
964
|
+
//
|
|
965
|
+
// Resolved BEFORE the adopt branch (task 1002562) because BOTH paths need it: an adopted
|
|
966
|
+
// repo has to be `npm ci`-installable by the provisioner exactly like a scaffolded one.
|
|
919
967
|
const upgrade = require('./upgrade');
|
|
920
|
-
let coreDep = null; // null → writeConfigs
|
|
968
|
+
let coreDep = null; // null → writeConfigs/layerPackageJson write the caret range
|
|
921
969
|
let vendorPlan = null;
|
|
922
970
|
if (vendorCore) {
|
|
923
971
|
const tgz = path.resolve(vendorCore);
|
|
@@ -935,6 +983,10 @@ async function main(argv) {
|
|
|
935
983
|
vendorPlan = { tgz, version, vendorRel };
|
|
936
984
|
}
|
|
937
985
|
|
|
986
|
+
// Brownfield (ADR 0121): a distinct LAYER + real-state path — never the greenfield
|
|
987
|
+
// scaffold/overwrite below. Its own report + next-steps live in runAdopt.
|
|
988
|
+
if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept, coreDep, vendorPlan });
|
|
989
|
+
|
|
938
990
|
console.log(`\nScaffolding "${spec.identity.productName}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
|
|
939
991
|
writeConfigs(spec, { dir, force, dryRun, coreDep });
|
|
940
992
|
|
|
@@ -1024,6 +1076,9 @@ async function main(argv) {
|
|
|
1024
1076
|
else console.log(' (--no-seed: skipped version/task seeding)');
|
|
1025
1077
|
|
|
1026
1078
|
console.log(`\n${dryRun ? 'Dry run complete.' : 'Done.'} Next:`);
|
|
1079
|
+
if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
|
|
1080
|
+
console.log(` ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
|
|
1081
|
+
}
|
|
1027
1082
|
console.log(' • fill the look: hand docs/branding-fill-prompt.md to your own LLM');
|
|
1028
1083
|
console.log(` • sign in as ${spec.firstAdmin} to become the owner, then run \`bongos start\` to see the kickoff tasks`);
|
|
1029
1084
|
console.log(' • work through the kickoff tasks the way you would any task: claim → ship');
|
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
// node scripts/gds/main-audit.js --revert # also print `git revert` plan
|
|
35
35
|
// node scripts/gds/main-audit.js --strict # also block on unresolved authors
|
|
36
36
|
// node scripts/gds/main-audit.js --db # resolve ranks from Postgres instead of the API
|
|
37
|
+
// node scripts/gds/main-audit.js --require-roster # CI: an empty roster is a loud DID-NOT-RUN, not a vacuous green
|
|
37
38
|
|
|
38
39
|
'use strict';
|
|
39
40
|
|
|
40
41
|
const { execSync } = require('node:child_process');
|
|
41
42
|
const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
|
|
42
|
-
const { cliClient } = require('./cli-lib');
|
|
43
|
+
const { cliClient, API_BASE } = require('./cli-lib');
|
|
43
44
|
|
|
44
45
|
// ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
|
|
45
46
|
|
|
@@ -87,7 +88,7 @@ function analyzeCommits(commits, rankByIdentity, { strict = false } = {}) {
|
|
|
87
88
|
author: c.authorName,
|
|
88
89
|
rank,
|
|
89
90
|
files: hits.map((h) => h.file),
|
|
90
|
-
summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by
|
|
91
|
+
summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by a Metic+ builder if genuinely needed.`,
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -120,6 +121,34 @@ function git(args) {
|
|
|
120
121
|
return execSync(`git ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// How far back the default scan reaches, in first-parent steps.
|
|
125
|
+
const DEFAULT_WINDOW = 200;
|
|
126
|
+
|
|
127
|
+
// First-parent depth of a ref. `<ref>~N` walks FIRST parents N times, so this —
|
|
128
|
+
// not the total commit count — is what bounds the window. On a repo where every
|
|
129
|
+
// land is a --no-ff merge the two diverge sharply: this one has 526 commits
|
|
130
|
+
// reachable from origin/main but a first-parent chain only 186 deep.
|
|
131
|
+
function firstParentDepth(ref) {
|
|
132
|
+
try {
|
|
133
|
+
return Number(git(`rev-list --count --first-parent ${ref}`).trim());
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return NaN;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Clamp the window to what the history actually holds. Returns null when there
|
|
140
|
+
// is nothing to audit, so the caller reports that plainly instead of emitting a
|
|
141
|
+
// `<ref>~200..<ref>` that git rejects as an unknown revision (task 1002566).
|
|
142
|
+
function clampWindow(depth, want = DEFAULT_WINDOW) {
|
|
143
|
+
if (typeof depth !== 'number' || !Number.isFinite(depth) || depth < 2) return null;
|
|
144
|
+
return Math.min(want, depth - 1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function defaultRange(ref, depth, want = DEFAULT_WINDOW) {
|
|
148
|
+
const n = clampWindow(depth, want);
|
|
149
|
+
return n === null ? null : `${ref}~${n}..${ref}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
123
152
|
// Parse `git log --name-only` with a NUL-delimited custom format into commits.
|
|
124
153
|
function parseCommits(range) {
|
|
125
154
|
// %x00 = NUL. Format: NUL sha NUL parents NUL authorName NUL authorEmail NUL subject \n files...
|
|
@@ -128,7 +157,9 @@ function parseCommits(range) {
|
|
|
128
157
|
try {
|
|
129
158
|
out = git(`log ${range} --name-only --format=${JSON.stringify(fmt)}`);
|
|
130
159
|
} catch (e) {
|
|
131
|
-
|
|
160
|
+
// Say plainly that nothing was audited. A bare git error here used to read
|
|
161
|
+
// as a transient glitch, which is how a dead check passed for a clean one.
|
|
162
|
+
return { error: `DID NOT RUN — git log could not resolve range ${range}, so no commits were audited: ${e.message}` };
|
|
132
163
|
}
|
|
133
164
|
const records = out.split('\x00').slice(1); // first chunk is empty / pre-first-NUL
|
|
134
165
|
const commits = [];
|
|
@@ -194,8 +225,25 @@ async function fetchRankMap({ useDb = false } = {}) {
|
|
|
194
225
|
try {
|
|
195
226
|
const api = await cliClient({ allowUnauthenticated: true });
|
|
196
227
|
const res = await api.builders.getBuildersRoster();
|
|
197
|
-
if (res && res.ok)
|
|
198
|
-
|
|
228
|
+
if (res && res.ok) {
|
|
229
|
+
const map = rowsToMap(res.data?.builders || []);
|
|
230
|
+
if (Object.keys(map).length) return map;
|
|
231
|
+
}
|
|
232
|
+
} catch (_) { /* offline / no session — fall through to the public surface */ }
|
|
233
|
+
// Public fallback (task 1002612): CI has no GDS session, and with an empty
|
|
234
|
+
// rank map every author resolves to null so a blocker can NEVER fire — a
|
|
235
|
+
// vacuous green. github_login + rank are public facts on this platform (the
|
|
236
|
+
// status dashboard serves them), and the public leaderboard is server-
|
|
237
|
+
// authoritative, so resolving from it is as trustworthy as the roster route.
|
|
238
|
+
// Caveat: a zero-activity builder may be absent → resolves as UNRESOLVED
|
|
239
|
+
// ('review' finding), never as falsely-trusted.
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(`${API_BASE}/api/gds/public/leaderboard`);
|
|
242
|
+
if (res.ok) {
|
|
243
|
+
const data = await res.json();
|
|
244
|
+
return rowsToMap(data?.leaderboard || []);
|
|
245
|
+
}
|
|
246
|
+
} catch (_) { /* offline — map stays empty → unresolved authors */ }
|
|
199
247
|
return {};
|
|
200
248
|
}
|
|
201
249
|
|
|
@@ -221,12 +269,22 @@ async function main() {
|
|
|
221
269
|
range = process.env.OTB_AUDIT_RANGE;
|
|
222
270
|
}
|
|
223
271
|
if (!range || range === true) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else {
|
|
272
|
+
const ref = verifyHead ? 'HEAD' : 'origin/main';
|
|
273
|
+
if (!verifyHead) {
|
|
227
274
|
// Default: recent origin/main. Fetch first so we're auditing the truth.
|
|
228
275
|
try { git('fetch origin main'); } catch (_) { /* offline — audit local */ }
|
|
229
|
-
|
|
276
|
+
}
|
|
277
|
+
// Clamp to the ref's real first-parent depth. A fixed ~200 silently made
|
|
278
|
+
// this audit unrunnable on any repo with a shorter chain (task 1002566).
|
|
279
|
+
const depth = firstParentDepth(ref);
|
|
280
|
+
range = defaultRange(ref, depth);
|
|
281
|
+
if (!range) {
|
|
282
|
+
console.error(
|
|
283
|
+
`main-audit: DID NOT RUN — cannot build a default range for ${ref}: its first-parent history is `
|
|
284
|
+
+ `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
|
|
285
|
+
+ 'Pass --range <A..B> or set OTB_AUDIT_RANGE. This is a check that did not execute, not a clean audit.'
|
|
286
|
+
);
|
|
287
|
+
process.exit(2);
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
@@ -237,7 +295,15 @@ async function main() {
|
|
|
237
295
|
}
|
|
238
296
|
const rankMap = await fetchRankMap({ useDb });
|
|
239
297
|
if (Object.keys(rankMap).length === 0) {
|
|
240
|
-
|
|
298
|
+
// --require-roster (task 1002612): the CI caller refuses to "pass" an audit
|
|
299
|
+
// that could not have failed. With no ranks, every author is unresolved and
|
|
300
|
+
// a blocker can never fire — that is a check that did not execute, not a
|
|
301
|
+
// clean audit (the same false-green shape parseCommits guards against).
|
|
302
|
+
if (process.argv.includes('--require-roster')) {
|
|
303
|
+
console.error('main-audit: DID NOT RUN — the roster is unavailable (offline / no session / public leaderboard unreachable). An empty rank map can never produce a blocker, so exiting green would be a vacuous pass. Fix connectivity or drop --require-roster for a best-effort local read.');
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
console.error('main-audit: WARNING — could not load the GDS roster (offline / insufficient rank / no DB). Authors will be unresolved; run as a Metic+ builder or with --db (on the droplet) for a definitive audit.');
|
|
241
307
|
}
|
|
242
308
|
|
|
243
309
|
const result = analyzeCommits(parsed.commits, rankMap, { strict });
|
|
@@ -276,4 +342,7 @@ if (require.main === module) {
|
|
|
276
342
|
});
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
module.exports = {
|
|
345
|
+
module.exports = {
|
|
346
|
+
analyzeCommits, resolveRank, parseCommits, rowsToMap,
|
|
347
|
+
clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
|
|
348
|
+
};
|
|
@@ -108,6 +108,12 @@ function isPublicRoutableIPv6(s) {
|
|
|
108
108
|
if (!g) return false;
|
|
109
109
|
if (g.slice(0, 7).every((x) => x === 0)) return false; // :: (unspecified) and ::1 (loopback)
|
|
110
110
|
const h = g[0];
|
|
111
|
+
// ::/8 is IETF-RESERVED (RFC 4291 §2.4) — no host is reachable there, so an address
|
|
112
|
+
// in it is never a real leak. Excluding it is what stops degenerate shorthand like
|
|
113
|
+
// `5::` / `c9::` from matching: those are 2-3 chars that random BINARY bytes hit
|
|
114
|
+
// constantly, which flagged 7 phantom "public-ip" leaks across committed .png/.webp
|
|
115
|
+
// assets and left the C9 full-tree mirror gate permanently red (task 1002470).
|
|
116
|
+
if (h < 0x0100) return false; // reserved ::/8
|
|
111
117
|
if (h >= 0xfe80 && h <= 0xfebf) return false; // link-local fe80::/10
|
|
112
118
|
if (h >= 0xfc00 && h <= 0xfdff) return false; // ULA fc00::/7
|
|
113
119
|
if (h >= 0xff00) return false; // multicast ff00::/8
|
|
@@ -377,7 +377,7 @@ function report(summary, FLAGS, { checkOnly }) {
|
|
|
377
377
|
if (checkOnly) { console.log('(--check: no stamping)'); return; }
|
|
378
378
|
if (summary.noop) { console.log('All chores at or above floor — nothing to stamp.'); return; }
|
|
379
379
|
|
|
380
|
-
console.log(`\nReview gate: ${summary.autoPromote ? "AUTO-PROMOTE (copies land status='ready', live now)" : "REVIEW (copies land status='backlog';
|
|
380
|
+
console.log(`\nReview gate: ${summary.autoPromote ? "AUTO-PROMOTE (copies land status='ready', live now)" : "REVIEW (copies land status='backlog'; a Metic+ builder promotes to 'ready')"}${FLAGS.dryRun ? ' [DRY RUN — nothing created]' : ''}`);
|
|
381
381
|
for (const g of summary.stamped) {
|
|
382
382
|
const who = `${g.discipline}/${g.template_id}`;
|
|
383
383
|
if (g.error) console.log(` ✗ ${who}: ${g.error}`);
|
|
@@ -386,7 +386,7 @@ function report(summary, FLAGS, { checkOnly }) {
|
|
|
386
386
|
else console.log(` ✓ ${who}: task #${g.task_id} [${g.status}] "${g.title}" (ref ${g.source_ref})`);
|
|
387
387
|
}
|
|
388
388
|
if (!summary.autoPromote && !FLAGS.dryRun && summary.stamped.some((g) => g.task_id)) {
|
|
389
|
-
console.log('\nNext:
|
|
389
|
+
console.log('\nNext: a Metic+ builder promotes the new backlog copies to ready:');
|
|
390
390
|
console.log(' node scripts/gds/api.js POST /api/gds/tasks/<id>/promote (or the builders\' hall)');
|
|
391
391
|
}
|
|
392
392
|
}
|