@bongos/core 1.21.176 → 1.21.177

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.
Files changed (103) hide show
  1. package/.bongos-core.json +61 -361
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/config/scheduled-routines.json +3 -8
  4. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  5. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  6. package/docs/canonical-permissions.md +1 -1
  7. package/docs/file-map.md +3 -8
  8. package/docs/handoff-template.md +1 -1
  9. package/docs/module-api-changelog.md +2 -0
  10. package/docs/onboarding/diagrams/README.md +3 -7
  11. package/docs/onboarding/diagrams/assertions.json +3 -3
  12. package/docs/recipes/ops-gotchas.md +3 -1
  13. package/modules/autonomy/db.js +9 -0
  14. package/modules/autonomy/module.json +2 -2
  15. package/modules/autonomy/pollers/daily-checks.js +262 -0
  16. package/modules/autonomy/pollers/routine-timers.js +7 -5
  17. package/modules/autonomy/rank-roles.js +64 -0
  18. package/modules/autonomy/server-schedule.js +37 -0
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/release-notes.json +14 -0
  22. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  23. package/scripts/bongos/audit-authorship.sh +0 -5
  24. package/scripts/bongos/audit-rank-roles.js +15 -56
  25. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  26. package/scripts/bongos/diagram-facts.js +1 -1
  27. package/scripts/bongos/do-api.js +1 -1
  28. package/scripts/bongos/fitness-checks-identity.js +4 -15
  29. package/scripts/bongos/gen-diagrams.js +3 -3
  30. package/scripts/bongos/pulse.js +5 -3
  31. package/scripts/bongos/routine-schedule.js +38 -3
  32. package/scripts/bongos/ship-regen.js +2 -3
  33. package/scripts/security/dep-audit.js +50 -124
  34. package/src/module-api.js +1 -1
  35. package/tests/autonomy_daily_checks.mjs +201 -0
  36. package/tests/claude_materialize.mjs +12 -12
  37. package/tests/effective_visibility_predicate.mjs +0 -18
  38. package/tests/routine_capture_kind.mjs +18 -7
  39. package/tests/routine_schedule.mjs +42 -0
  40. package/scripts/bongos/artifact-staleness.js +0 -267
  41. package/scripts/bongos/classify-discipline.js +0 -153
  42. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  43. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  44. package/scripts/bongos/goal-names.js +0 -102
  45. package/scripts/bongos/rename-goals.js +0 -109
  46. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  47. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  48. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  49. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  50. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  51. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  52. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  53. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  54. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  55. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  56. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  57. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  58. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  59. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  60. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  61. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  62. package/scripts/bongos/seed-governance-tasks.js +0 -340
  63. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  64. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  65. package/scripts/bongos/seed-productization-tasks.js +0 -235
  66. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  67. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  68. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  69. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  70. package/scripts/gds/artifact-staleness.js +0 -7
  71. package/scripts/gds/classify-discipline.js +0 -7
  72. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  73. package/scripts/gds/coreB-carve-proof.sh +0 -8
  74. package/scripts/gds/goal-names.js +0 -7
  75. package/scripts/gds/rename-goals.js +0 -7
  76. package/scripts/gds/seed-adopt-tasks.js +0 -7
  77. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  78. package/scripts/gds/seed-board-room-tasks.js +0 -6
  79. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  80. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  81. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  82. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  83. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  84. package/scripts/gds/seed-bongos-tasks.js +0 -7
  85. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  86. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  87. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  88. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  89. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  90. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  91. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  92. package/scripts/gds/seed-governance-tasks.js +0 -7
  93. package/scripts/gds/seed-medusa-tasks.js +0 -7
  94. package/scripts/gds/seed-module-upstreaming.js +0 -7
  95. package/scripts/gds/seed-productization-tasks.js +0 -7
  96. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  97. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  98. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  99. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  100. package/scripts/migrate_cost_ledger.sh +0 -178
  101. package/tests/artifact_staleness.mjs +0 -207
  102. package/tests/goal_names.mjs +0 -120
  103. package/tests/newcomer_generator.mjs +0 -48
@@ -1,267 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/bongos/artifact-staleness.js — are the gitignored generated artifacts on
3
- // THIS machine actually current with the commit it has checked out? (task 1001930,
4
- // ADR 0110 pt.5)
5
- //
6
- // WHY THIS EXISTS. ADR 0110 moved `docs/repo-map.md` and `docs/session-log-index.md`
7
- // off the git graph: they are 100%-generated, two parallel branches collided on them
8
- // constantly, so they became untracked files regenerated at deploy. That trade was
9
- // deliberate, and the ADR names the debt it took on in its own Consequences:
10
- //
11
- // "A generator that breaks at deploy leaves a stale/absent file with no blocking
12
- // backstop; the LOUD ::ERROR:: line is the alarm (mandatory, not optional)."
13
- //
14
- // That alarm is a line in the droplet's journald. Nobody reads journald. The per-PR
15
- // freshness gate cannot help either — the files are gitignored, so they never appear
16
- // in a diff for CI to check (`--check-whole`'s own comment says it plainly: "used by
17
- // nothing hard; deploy owns it"). So if the deploy regen step silently stops running,
18
- // every agent keeps reading a symbol map of a codebase that has moved on, and nothing
19
- // anywhere says so. This check is the backstop that posture depends on.
20
- //
21
- // WHAT IT COMPARES. For each registered artifact, its own generator's `--check-whole`
22
- // answers "would regenerating this file change it?" — run against the checkout that is
23
- // actually serving. A difference means the deploy generator did not run, or ran and
24
- // broke. Asking the generator rather than reimplementing the comparison is the point:
25
- // there is exactly one definition of "fresh" per artifact and it lives with the thing
26
- // that writes it, so this check cannot drift away from what deploy produces.
27
- //
28
- // ABSENT AND STALE ARE REPORTED SEPARATELY, and the split is operational, not cosmetic:
29
- // absent means the regen step never ran here at all (a deploy line was dropped, or a
30
- // fresh clone was never generated into), stale means it ran once and has not since.
31
- // Those send someone to two different places, so the report must not blur them.
32
- //
33
- // ── WHERE THIS MAY RUN — read before scheduling it anywhere new ───────────────
34
- //
35
- // ONLY where the artifacts are SUPPOSED to already exist: a deployed instance after its
36
- // source-fetch regen. NEVER in CI. A fresh `git clone` legitimately
37
- // has no `docs/repo-map.md` — it is gitignored, so it is not in the repository at all
38
- // — and this check would report it absent on every single run. A monitor that is red
39
- // every run teaches its reader to ignore it, which is the exact failure ADR 0110's
40
- // warn-only posture already has. That is why this ships as a scheduled routine on the
41
- // instance and NOT as a `.github/workflows/` job, unlike its sibling publish-watch.js
42
- // (which reads git and the registry, and so is true anywhere).
43
- //
44
- // THE MAIN GUARD. "Fresh" here means "matches what THIS commit generates". On a
45
- // deployed instance that commit is main, because deploy does `git reset --hard
46
- // origin/main` — so the question the caller actually wants ("does the served artifact
47
- // lag main's HEAD?") and the question `--check-whole` answers are the same question.
48
- // In a feature worktree they are NOT: the artifact would be compared against that
49
- // branch, and a pass or a fail would both be meaningless. So a checkout that is not at
50
- // origin/main is INCONCLUSIVE (exit 2), never a finding — a monitor that cannot tell
51
- // which question it just answered must say so rather than guess.
52
- 'use strict';
53
-
54
- const { spawnSync } = require('child_process');
55
- const fs = require('fs');
56
- const path = require('path');
57
-
58
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
59
- const GIT_TIMEOUT_MS = 15000;
60
- const CHECK_TIMEOUT_MS = 120000;
61
-
62
- // The gitignored, whole-file generated artifacts this check covers.
63
- //
64
- // MEMBERSHIP RULE — an artifact belongs here when all three hold: it is gitignored (so
65
- // no PR gate can see it), it is written by a deploy-time generator (so it can silently
66
- // stop being written), and its generator exposes `--check-whole` (so freshness has one
67
- // definition, owned by the writer). tests/artifact_staleness.mjs pins this list against
68
- // .gitignore and against the generators' real flags, so an artifact that gains a
69
- // `--check-whole` and is left out of here fails that test rather than going unwatched.
70
- //
71
- // DELIBERATELY NOT HERE, and each for a reason that is not "we forgot":
72
- // • modules/hall-ui/public/atlas.json (gen-atlas.js, ADR 0094) — gitignored and
73
- // deploy-generated, but its payload embeds `generatedAt: new Date()`, so a
74
- // regenerate-and-compare differs on every run by construction. Checking it needs a
75
- // comparison over `views` only, or a freshness-by-age test against the last deploy
76
- // — a different mechanism, not this one. gen-atlas has no --check mode at all today.
77
- // • docs/onboarding/diagrams/*.svg|png — rendering them needs a headless Chromium the
78
- // droplet does not have (the reason ADR 0110 deferred the diagram half in the first
79
- // place), so freshness cannot be evaluated where this runs. Their FACTS are already
80
- // backstopped by the diagram-drift routine, which reads assertions.json instead.
81
- const ARTIFACTS = Object.freeze([
82
- Object.freeze({
83
- rel: 'docs/repo-map.md',
84
- generator: 'gen-repo-map.js',
85
- what: 'the symbol skeleton agents navigate the codebase by (ADR 0063)',
86
- }),
87
- Object.freeze({
88
- rel: 'docs/session-log-index.md',
89
- generator: 'gen-session-index.js',
90
- what: 'the full session-log index the CLAUDE.md §13 snippet links out to (ADR 0062 §8)',
91
- }),
92
- ]);
93
-
94
- function git(args, run = spawnSync) {
95
- const raw = gitRaw(args, run);
96
- return raw === null ? null : raw.trim();
97
- }
98
-
99
- // Untrimmed, because `git status --porcelain` is column-significant: a line is
100
- // `XY<space>PATH` and an unstaged modification's X is a SPACE (" M path"). Trimming the
101
- // buffer eats that leading space on the FIRST line only, so a slice(3) then swallows the
102
- // path's first character — which is exactly how this printed "cripts/gds/…" until
103
- // tests/artifact_staleness.mjs caught it.
104
- function gitRaw(args, run = spawnSync) {
105
- const r = run('git', args, { cwd: REPO_ROOT, encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
106
- if (!r || r.error || r.status !== 0) return null;
107
- return String(r.stdout || '');
108
- }
109
-
110
- // The paths in a porcelain listing, status columns removed.
111
- function porcelainPaths(raw) {
112
- return raw.split('\n').filter((l) => l.length > 3).map((l) => l.slice(3).trim()).filter(Boolean);
113
- }
114
-
115
- // Is this checkout a fair thing to compare the artifacts against — at origin/main AND
116
- // clean? Returns { ok } or { ok:false, reason }; git being unable to answer is equally
117
- // inconclusive.
118
- //
119
- // The CLEAN half is not belt-and-braces, and it was found by running this check against
120
- // its own uncommitted source file: a generator regenerates from the WORKING TREE, so one
121
- // uncommitted file that moves a symbol makes repo-map.md "stale" against a main it in
122
- // fact matches. On the droplet the tree is clean by construction (deploy does
123
- // `git reset --hard`), so requiring it costs a correct run nothing — while without it
124
- // every hand-run from a working checkout invents a finding, which is precisely how a
125
- // monitor teaches its reader to ignore it. Gitignored files (the artifacts themselves)
126
- // never appear in `--porcelain`, so they cannot trip this.
127
- function atMain(run = spawnSync) {
128
- const head = git(['rev-parse', 'HEAD'], run);
129
- const main = git(['rev-parse', 'origin/main'], run);
130
- if (!head) return { ok: false, reason: 'could not read HEAD (not a git checkout?)' };
131
- if (!main) return { ok: false, reason: 'could not read origin/main (no remote ref here?)' };
132
- if (head !== main) {
133
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], run) || '(detached)';
134
- return { ok: false, reason: `checkout is at ${branch} ${head.slice(0, 8)}, not origin/main ${main.slice(0, 8)}` };
135
- }
136
- const dirty = gitRaw(['status', '--porcelain'], run);
137
- if (dirty === null) return { ok: false, reason: 'could not read the working tree state' };
138
- const paths = porcelainPaths(dirty);
139
- if (paths.length) {
140
- const shown = paths.slice(0, 3).join(', ');
141
- const more = paths.length > 3 ? `, +${paths.length - 3} more` : '';
142
- return { ok: false, reason: `working tree is not clean (${shown}${more}) — the generators would compare against those edits, not main` };
143
- }
144
- return { ok: true, sha: head };
145
- }
146
-
147
- // Ask ONE artifact's own generator whether it is current.
148
- // fresh exit 0 — regenerating would change nothing
149
- // stale exit 2 — it would (absent counts as stale; `present` says which)
150
- // unknown anything else — the generator itself could not answer, which is not a
151
- // finding about the artifact and must never be reported as one.
152
- function checkArtifact(a, { root = REPO_ROOT, run = spawnSync, exists = fs.existsSync } = {}) {
153
- const present = exists(path.join(root, a.rel));
154
- const r = run(process.execPath, [path.join(root, 'scripts', 'bongos', a.generator), '--check-whole'],
155
- { cwd: root, encoding: 'utf8', timeout: CHECK_TIMEOUT_MS });
156
- if (!r || r.error) {
157
- return { ...a, state: 'unknown', present, detail: r && r.error ? r.error.message : 'generator did not run' };
158
- }
159
- if (r.status === 0) return { ...a, state: 'fresh', present };
160
- if (r.status === 2) return { ...a, state: present ? 'stale' : 'absent', present };
161
- const said = String(r.stderr || r.stdout || '').trim().split('\n').pop() || `exit ${r.status}`;
162
- return { ...a, state: 'unknown', present, detail: said };
163
- }
164
-
165
- function check({ root = REPO_ROOT, run = spawnSync, exists = fs.existsSync, artifacts = ARTIFACTS, requireMain = true } = {}) {
166
- if (requireMain) {
167
- const m = atMain(run);
168
- if (!m.ok) { const e = new Error(m.reason); e.inconclusive = true; throw e; }
169
- }
170
- const results = artifacts.map((a) => checkArtifact(a, { root, run, exists }));
171
- return {
172
- results,
173
- fresh: results.filter((r) => r.state === 'fresh'),
174
- stale: results.filter((r) => r.state === 'stale'),
175
- absent: results.filter((r) => r.state === 'absent'),
176
- unknown: results.filter((r) => r.state === 'unknown'),
177
- };
178
- }
179
-
180
- // Plain language: this lands in Discord #ship-news, where the audience is the owner,
181
- // not whoever wrote the generator.
182
- function report(r) {
183
- const bad = [...r.absent, ...r.stale];
184
- if (!bad.length && !r.unknown.length) {
185
- return `Generated navigation docs are current — ${r.fresh.length} artifact(s) match this commit.`;
186
- }
187
- const lines = [];
188
- if (bad.length) {
189
- lines.push(`⚠ ${bad.length} generated file(s) on this instance no longer match the code being served.`);
190
- lines.push('');
191
- for (const a of r.absent) lines.push(` • ${a.rel} — MISSING entirely. Nothing has generated it here. It is ${a.what}.`);
192
- for (const a of r.stale) lines.push(` • ${a.rel} — out of date: it no longer matches what this commit would produce. It is ${a.what}.`);
193
- lines.push('');
194
- lines.push('These are rebuilt by the deploy step, so this means that step stopped running or broke.');
195
- lines.push('Nothing blocks on them, which is why it went unreported until now — anyone reading these');
196
- lines.push('files is being given an out-of-date picture. Check the deploy log for its regen lines');
197
- lines.push('(they print ::ERROR:: when a generator fails).');
198
- }
199
- if (r.unknown.length) {
200
- if (bad.length) lines.push('');
201
- lines.push(`Could not check ${r.unknown.length} artifact(s):`);
202
- for (const a of r.unknown) lines.push(` • ${a.rel} — ${a.detail}`);
203
- }
204
- return lines.join('\n');
205
- }
206
-
207
- // Deliver the finding here rather than through run-routine.js's `sink: discord`.
208
- // run-routine treats a non-zero step as a FAILED step and aborts BEFORE it routes
209
- // stdout, so exiting 1 on a finding would throw that finding away. publish-watch.js
210
- // and artifact-audit.js carry the same shape for the same reason; their routine steps
211
- // all stay `sink: none` and the script performs its own side effect.
212
- async function deliver(text) {
213
- const { postWebhook } = require(path.join(__dirname, '..', 'discord', 'lib.js'));
214
- await postWebhook('ship_news', text);
215
- }
216
-
217
- async function main() {
218
- const argv = process.argv.slice(2);
219
- const json = argv.includes('--json');
220
-
221
- let r;
222
- try {
223
- r = check({ requireMain: !argv.includes('--any-ref') });
224
- } catch (err) {
225
- // Exit 2 = COULD NOT RUN, kept distinct from 1 (ran, found something) so a
226
- // scheduler can go red on the former and merely report the latter.
227
- console.error(`artifact-staleness could not complete: ${err && err.message ? err.message : err}`);
228
- if (err && err.inconclusive) {
229
- console.error(' Freshness is only meaningful against the commit being served. Re-run on the');
230
- console.error(' deployed instance, or pass --any-ref to compare against whatever is checked out.');
231
- }
232
- process.exit(2);
233
- }
234
-
235
- const text = report(r);
236
- const found = r.stale.length + r.absent.length;
237
-
238
- if (json) { console.log(JSON.stringify(r, null, 2)); process.exit(found ? 1 : 0); }
239
-
240
- // A healthy run is deliberately quiet — it tells a person who ran it by hand
241
- // (stderr) and posts nothing. A daily "all fine" is how a channel becomes wallpaper.
242
- if (!found) {
243
- console.error(text);
244
- // An artifact we could not check is not a finding, but it must not be silent
245
- // either: a permanently-unknown artifact is unwatched without anyone deciding so.
246
- process.exit(r.unknown.length ? 2 : 0);
247
- }
248
-
249
- console.log(text);
250
- if (argv.includes('--no-post')) process.exit(1);
251
-
252
- try {
253
- await deliver(text);
254
- console.error('posted the finding to Discord #ship-news.');
255
- process.exit(1);
256
- } catch (err) {
257
- // Found something and could not say it — the original defect one layer down, so
258
- // it is the LOUDEST outcome: exit 3 is >= 2, which turns the scheduled run red.
259
- console.error(`\nFOUND STALE ARTIFACTS AND COULD NOT DELIVER THE FINDING: ${err && err.message ? err.message : err}`);
260
- console.error('The finding is above. Set DISCORD_SHIP_NEWS_WEBHOOK so this reaches #ship-news.');
261
- process.exit(3);
262
- }
263
- }
264
-
265
- if (require.main === module) main();
266
-
267
- module.exports = { ARTIFACTS, atMain, checkArtifact, check, report };
@@ -1,153 +0,0 @@
1
- #!/usr/bin/env node
2
- // classify-discipline — bulk-classify tasks.discipline via the heuristic
3
- // classifier in src/bongos/task-classifier.js.
4
- //
5
- // Usage:
6
- // node scripts/bongos/classify-discipline.js [--commit] [--all] [--limit N]
7
- //
8
- // Default mode: dry-run. Walks every task whose discipline is currently
9
- // 'unclassified', runs classifyTaskDiscipline() over title + description +
10
- // touches, and prints a table of proposed classifications. Nothing is
11
- // written to the DB.
12
- //
13
- // With --commit: same walk, but PATCH /tasks/:id for every proposal that
14
- // came back as engineer, artist, or ideator. Tasks the classifier leaves as
15
- // 'unclassified' are skipped (left for hand-triage).
16
- //
17
- // With --all: include tasks that already have a non-unclassified
18
- // discipline. Useful for re-running after tweaking the classifier rules;
19
- // will overwrite existing classifications. NOT the default.
20
- //
21
- // Idempotent without --all: re-running after a successful --commit is a
22
- // no-op because every previously-classified row falls outside the filter.
23
-
24
- const { cliClient, requireSession } = require('./cli-lib');
25
- // task-classifier carved to modules/lifecycle/ (BV1.R86); a CLI may require a
26
- // module directly (outside the kernel boundary — the grading/economy precedent).
27
- const { classifyTaskDiscipline } = require('../../modules/lifecycle/task-classifier');
28
-
29
- function pad(s, n) {
30
- s = String(s);
31
- return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length);
32
- }
33
-
34
- function emoji(d) {
35
- if (d === 'artist') return '🎨';
36
- if (d === 'ideator') return '💡';
37
- if (d === 'engineer') return '🔧';
38
- return '· ';
39
- }
40
-
41
- (async () => {
42
- await requireSession();
43
- const args = process.argv.slice(2);
44
- const commit = args.includes('--commit');
45
- const all = args.includes('--all');
46
- const limitIdx = args.indexOf('--limit');
47
- const limit = limitIdx >= 0 ? Number(args[limitIdx + 1]) : Infinity;
48
-
49
- // Fetch tasks. Without --all we ask the API for only unclassified rows
50
- // so we don't have to pull every shipped task to triage them later.
51
- const api = await cliClient();
52
- const r = await api.tasks.getTasks(all ? {} : { query: { discipline: 'unclassified' } });
53
- if (!r.ok) {
54
- console.error(`failed (${r.status}):`, r.data);
55
- process.exit(1);
56
- }
57
- const rawTasks = (r.data?.tasks || []).slice(0, limit);
58
- // Skip __smoke__ test debris — these aren't real work, classifying them
59
- // would just pollute the discipline filter. Mig 005 already filters them
60
- // from claimable_tasks / version_progress on the public side.
61
- const smokeFiltered = rawTasks.filter((t) => /^__smoke__/.test(t.title || ''));
62
- const tasks = rawTasks.filter((t) => !/^__smoke__/.test(t.title || ''));
63
- if (tasks.length === 0) {
64
- console.log('nothing to classify — every task already has a discipline.');
65
- if (smokeFiltered.length) {
66
- console.log(`(skipped ${smokeFiltered.length} __smoke__ debris rows)`);
67
- }
68
- return;
69
- }
70
-
71
- // Run classifier locally — no API round-trip per task.
72
- const proposals = tasks.map((t) => {
73
- const result = classifyTaskDiscipline(t.title, t.description, t.touches);
74
- return { task: t, ...result };
75
- });
76
-
77
- // Print summary table grouped by discipline.
78
- const buckets = {
79
- engineer: proposals.filter((p) => p.discipline === 'engineer'),
80
- artist: proposals.filter((p) => p.discipline === 'artist'),
81
- ideator: proposals.filter((p) => p.discipline === 'ideator'),
82
- unclassified: proposals.filter((p) => p.discipline === 'unclassified'),
83
- };
84
-
85
- console.log('');
86
- console.log(`Proposed classifications across ${proposals.length} tasks:`);
87
- console.log(` ${emoji('engineer')} engineer ${buckets.engineer.length}`);
88
- console.log(` ${emoji('artist')} artist ${buckets.artist.length}`);
89
- console.log(` ${emoji('ideator')} ideator ${buckets.ideator.length}`);
90
- console.log(` ${emoji('unclassified')} unclassified ${buckets.unclassified.length} (will be skipped)`);
91
- if (smokeFiltered.length) {
92
- console.log(` 🧪 __smoke__ ${smokeFiltered.length} (filtered as test debris, not classified)`);
93
- }
94
- console.log('');
95
-
96
- // Detail by bucket. Within each bucket: highest-confidence first so
97
- // the strong signals are at the top — easy to spot the weakest.
98
- for (const bucket of ['engineer', 'artist', 'ideator']) {
99
- if (!buckets[bucket].length) continue;
100
- console.log(`--- ${bucket} ---`);
101
- const sorted = buckets[bucket].slice().sort((a, b) => b.confidence - a.confidence);
102
- for (const p of sorted) {
103
- const { task: t } = p;
104
- const version = pad(`[${t.version_id || '?'}]`, 10);
105
- const conf = `(${p.confidence.toFixed(2)})`;
106
- const title = (t.title || '').slice(0, 80);
107
- console.log(` #${pad(t.id, 4)} ${conf} ${version} ${title}`);
108
- }
109
- console.log('');
110
- }
111
-
112
- if (buckets.unclassified.length) {
113
- console.log('--- unclassified (signals too weak / balanced) ---');
114
- for (const p of buckets.unclassified.slice(0, 20)) {
115
- const { task: t } = p;
116
- const version = pad(`[${t.version_id || '?'}]`, 10);
117
- const title = (t.title || '').slice(0, 80);
118
- console.log(` #${pad(t.id, 4)} ${version} ${title}`);
119
- }
120
- if (buckets.unclassified.length > 20) {
121
- console.log(` ... ${buckets.unclassified.length - 20} more`);
122
- }
123
- console.log('');
124
- }
125
-
126
- if (!commit) {
127
- console.log('Dry-run only. Re-run with --commit to write classifications via PATCH.');
128
- return;
129
- }
130
-
131
- // Commit: PATCH each non-unclassified proposal.
132
- const toWrite = [...buckets.engineer, ...buckets.artist, ...buckets.ideator];
133
- console.log(`Committing ${toWrite.length} classifications...`);
134
- let ok = 0;
135
- let failed = 0;
136
- for (const p of toWrite) {
137
- const res = await api.tasks.patchTasksId({
138
- id: p.task.id,
139
- body: { discipline: p.discipline },
140
- });
141
- if (res.ok) {
142
- ok++;
143
- } else {
144
- failed++;
145
- console.error(` PATCH #${p.task.id} failed (${res.status}):`, res.data?.error || res.data);
146
- }
147
- }
148
- console.log('');
149
- console.log(`Done. ${ok} updated, ${failed} failed.`);
150
- })().catch((err) => {
151
- console.error('fatal:', err);
152
- process.exit(1);
153
- });
@@ -1,145 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/gds/codemod-rename-src-gds.js
3
- //
4
- // One-shot, auditable codemod for task 2004 (BV1.R124): rename the core source
5
- // directory src/gds/ -> src/bongos/ and rewrite EVERY in-repo reference to it.
6
- // Committed so the approach is reproducible and reviewable (task done-when).
7
- //
8
- // Deterministic by construction:
9
- // 1. `git mv src/gds src/bongos` (idempotent -- skipped if already moved).
10
- // 2. Over every TRACKED text file, rewrite the literal path token `src/gds`
11
- // -> `src/bongos`, guarded by a TRAILING boundary so look-alike tokens are
12
- // never clobbered:
13
- // src/gds/... -> src/bongos/... (directory refs)
14
- // "src/gds" -> "src/bongos" (bare, at a boundary)
15
- // src/gds-client -> src/gds-client (UNCHANGED: a look-alike file name)
16
- // src/gdsx -> src/gdsx (UNCHANGED here: prefix-edge fixture)
17
- //
18
- // Regex: /src\/gds(?![A-Za-z0-9_-])/g -- trailing guard only. No LEADING guard:
19
- // `scripts/gds` contains no `src/gds` substring, and the two real leading-glue
20
- // sites (`keyed-to-src/gds` in an audit, `\tsrc/gds/auth.js` in a mirror-export
21
- // fixture) are genuine directory refs we DO want rewritten.
22
- //
23
- // Excluded from the sweep (see EXCLUDE):
24
- // - this script (its own source contains the search literal),
25
- // - tests/path_match.mjs + tests/touches_overlap.mjs -- they assert the
26
- // "src/gdsx must NOT match src/gds/" boundary edge; a mechanical half-rewrite
27
- // (which leaves `src/gdsx` but changes `src/gds/`) would silently gut the
28
- // test. Those two are hand-edited to src/bongos / src/bongosx so the edge
29
- // stays meaningful.
30
- //
31
- // git ls-files is the file source, so gitignored generated docs
32
- // (docs/repo-map.md, docs/session-log-index.md) are excluded -- they regenerate.
33
- // Binary files are skipped via a NUL-byte sniff.
34
- //
35
- // Idempotent: a second run moves nothing and rewrites nothing (0 replacements).
36
-
37
- const { execSync } = require('node:child_process');
38
- const fs = require('node:fs');
39
- const path = require('node:path');
40
-
41
- const REPO_ROOT = execSync('git rev-parse --show-toplevel').toString().trim();
42
-
43
- // (a) The slash form: `src/gds`, `src/gds/routes`, `"src/gds"` ... Trailing guard
44
- // only (see header) so `src/gds-client` and `src/gdsx` are left alone.
45
- const NEEDLE = /src\/gds(?![A-Za-z0-9_-])/g;
46
- const REPLACEMENT = 'src/bongos';
47
-
48
- // (b) The SPLIT-SEGMENT form: `path.join(root, 'src', 'gds', ...)` -- the segments
49
- // are separate string args, so there is no literal `src/gds` substring for (a) to
50
- // catch. Anchored on a PRECEDING quoted `'src'` segment so it rewrites only the
51
- // `'gds'` that follows `'src'` -- NEVER `'scripts', 'gds'` (scripts/gds is not
52
- // renamed). \s between segments tolerates a newline-split path.join. The `gds`
53
- // quote must close immediately, so `'gdsx'` / `'gds-client'` never match.
54
- const SEG = /(['"])src\1(\s*,\s*)(['"])gds\3/g;
55
- const segReplace = (s) => s.replace(SEG, (_m, q1, sep, q2) => `${q1}src${q1}${sep}${q2}bongos${q2}`);
56
-
57
- // (c) RELATIVE requires into the moved dir. A file DIRECTLY under src/ addresses
58
- // its former sibling as `require('./gds/x')` -- no `src/` prefix, so (a) misses it,
59
- // and after the move it dangles. `./gds` resolves to src/gds ONLY from a src-top-
60
- // level file; a `./gds` in scripts/* means scripts/gds (NOT renamed) and a deeper
61
- // src/**/* file resolves elsewhere -- so this pass is gated to `src/<file>` and
62
- // never fires elsewhere. (Verified: the only matches are src/module-api.js +
63
- // src/platform-server.js; no `../gds` relative forms exist.) `(?=\/|['"])` keeps
64
- // `./gds-client` / `./gdsx` safe.
65
- const isSrcTopLevel = (rel) => /^src\/[^/]+\.(?:js|mjs|cjs)$/.test(rel);
66
- const REL = /(\.\/)gds(?=\/|['"])/g;
67
-
68
- // (d) REGEX-ESCAPED slash form: `src\/gds` (backslash-slash) as written inside a
69
- // RegExp literal -- e.g. route-rank-check.js's ROUTE_FILE_RE and the tests that
70
- // match against it. `\/` is not the plain `/` (a) scans for, so (a) misses it.
71
- const ESC = /src\\\/gds(?![A-Za-z0-9_-])/g;
72
-
73
- // (e) WINDOWS double-backslash form: `src\\gds` in JS source (one backslash on
74
- // disk-per-escape, two chars) -- Windows-path fixtures the matchers normalize
75
- // (permission_path / publish_manifest). Trailing guard keeps `src\\gdsx` safe.
76
- const WIN = /src\\\\gds(?![A-Za-z0-9_-])/g;
77
-
78
- const EXCLUDE = new Set([
79
- 'scripts/gds/codemod-rename-src-gds.js',
80
- 'tests/path_match.mjs',
81
- 'tests/touches_overlap.mjs',
82
- ]);
83
-
84
- function gitMv() {
85
- const oldDir = path.join(REPO_ROOT, 'src', 'gds');
86
- const newDir = path.join(REPO_ROOT, 'src', 'bongos');
87
- if (fs.existsSync(newDir)) {
88
- console.log('git mv: src/bongos already exists -- skipping move (idempotent).');
89
- return;
90
- }
91
- if (!fs.existsSync(oldDir)) {
92
- console.log('git mv: neither src/gds nor src/bongos present -- nothing to move.');
93
- return;
94
- }
95
- execSync('git mv src/gds src/bongos', { cwd: REPO_ROOT, stdio: 'inherit' });
96
- console.log('git mv: src/gds -> src/bongos');
97
- }
98
-
99
- function isBinary(buf) {
100
- const n = Math.min(buf.length, 8192);
101
- for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
102
- return false;
103
- }
104
-
105
- function main() {
106
- gitMv();
107
-
108
- const tracked = execSync('git ls-files', { cwd: REPO_ROOT, maxBuffer: 128 * 1024 * 1024 })
109
- .toString().split('\n').filter(Boolean);
110
-
111
- let filesChanged = 0;
112
- let totalReplacements = 0;
113
- const changed = [];
114
-
115
- for (const rel of tracked) {
116
- if (EXCLUDE.has(rel)) continue;
117
- const abs = path.join(REPO_ROOT, rel);
118
- let buf;
119
- try { buf = fs.readFileSync(abs); } catch { continue; } // e.g. deleted-in-index
120
- if (isBinary(buf)) continue;
121
- const before = buf.toString('utf8');
122
- const slashN = (before.match(NEEDLE) || []).length;
123
- const segN = (before.match(SEG) || []).length;
124
- const escN = (before.match(ESC) || []).length;
125
- const winN = (before.match(WIN) || []).length;
126
- const relN = isSrcTopLevel(rel) ? (before.match(REL) || []).length : 0;
127
- if (slashN === 0 && segN === 0 && escN === 0 && winN === 0 && relN === 0) continue;
128
- let after = segReplace(before.replace(NEEDLE, REPLACEMENT));
129
- after = after.replace(ESC, 'src\\/bongos').replace(WIN, 'src\\\\bongos');
130
- if (relN > 0) after = after.replace(REL, '$1bongos');
131
- if (after === before) continue;
132
- fs.writeFileSync(abs, after);
133
- filesChanged++;
134
- const n = slashN + segN + escN + winN + relN;
135
- totalReplacements += n;
136
- changed.push({ rel, n });
137
- }
138
-
139
- changed.sort((a, b) => b.n - a.n);
140
- console.log(`\ncodemod: rewrote ${totalReplacements} reference(s) across ${filesChanged} file(s).`);
141
- for (const c of changed) console.log(` ${String(c.n).padStart(4)} ${c.rel}`);
142
- console.log('\nExcluded (hand-edited or self): ' + [...EXCLUDE].join(', '));
143
- }
144
-
145
- main();