@bongos/core 1.19.638 → 1.19.639

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.
@@ -101,6 +101,27 @@ function checkRolePacks(fsImpl = fs) {
101
101
  }
102
102
  }
103
103
 
104
+ // Each pack's WRITTEN-RESPONSIBILITY block must match src/role-responsibilities.js
105
+ // (task 1003732). It lives with the other pack assertions rather than in
106
+ // checkGeneratedArtifactsFresh so one check owns "the packs are correct" — and the
107
+ // drift matters for the same reason the missing-pack case does: the profile and
108
+ // the grader read the source at runtime and cannot drift, but the pack is markdown,
109
+ // so its copy is the only one that can — and it is the copy a SESSION is instructed
110
+ // by. A stale block means the project teaches one standard, shows a second on the
111
+ // profile, and grades against a third. Skipped when the fs is stubbed (the negative
112
+ // tests inject a registry that names packs this generator would not find).
113
+ if (fsImpl === fs) {
114
+ try {
115
+ const { plan } = require('./gen-role-responsibilities.js');
116
+ const { writes } = plan();
117
+ for (const w of writes) {
118
+ violations.push(`${w.rel}: the written-responsibility block is STALE against src/role-responsibilities.js — run \`node scripts/gds/gen-role-responsibilities.js\` and commit (task 1003732).`);
119
+ }
120
+ } catch (e) {
121
+ violations.push(`could not check the packs' written-responsibility blocks (${e && e.message}) — run \`node scripts/gds/gen-role-responsibilities.js --check\`.`);
122
+ }
123
+ }
124
+
104
125
  // The split's own shape, asserted so a later edit cannot quietly drop a craft
105
126
  // back into the kernel: three core packs, and Governor deliberately absent
106
127
  // (criterion wa6-kernel-and-packs — "three packs, not four, until the owner decides").
package/src/module-api.js CHANGED
@@ -41,6 +41,22 @@ const instanceConfig = require('./instance-config');
41
41
  const staleTimer = require('./stale-timer');
42
42
  const seams = require('./module-seams');
43
43
  const { buildInfo } = require('./build-info');
44
+ // Eager and DESTRUCTURED on purpose, and inside the rule above rather than an
45
+ // exception to it: role-responsibilities.js requires NOTHING (80 lines of the
46
+ // owner's text plus three pure lookups), so it costs one file read and drags no
47
+ // server file into a CLI subcommand's require-closure — the same footing as
48
+ // `branding` and `instanceConfig` here.
49
+ //
50
+ // It has to be a NAMED require rather than `require(...).x` read inside a getter,
51
+ // or CI's dead-code ratchet counts these exports as dead and no PR touching them
52
+ // can merge — task 1003732 stranded on exactly that. `responsibilityFor`'s only
53
+ // consumer is modules/grading/grader-prompt.js reaching through this doorway, and
54
+ // modules/ sits outside knip's `project` globs by design, so the import HERE is
55
+ // the only place that use can be seen. knip credits a destructured binding and
56
+ // not a member access on a namespace object, which is why the sibling exports
57
+ // (`responsibilitiesFor` via src/modules.js, `ROLE_RESPONSIBILITIES` via
58
+ // scripts/gds/gen-role-responsibilities.js) were never flagged and this one was.
59
+ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibilities');
44
60
 
45
61
  // ---------------------------------------------------------------------------
46
62
  // CORE_VERSION — the version of THIS published surface (semver).
@@ -55,7 +71,7 @@ const { buildInfo } = require('./build-info');
55
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
72
  // the entry to that file. Look for a version's history there, not here.
57
73
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.638'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.639'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
75
 
60
76
  // A namespaced logger so a module's log lines are attributable + consistent.
61
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -405,6 +421,19 @@ module.exports = {
405
421
  get pageMeta() { return require('./bongos/routes/_helpers').pageMeta; },
406
422
  get PAGINATION() { return require('./bongos/routes/_helpers').PAGINATION; },
407
423
 
424
+ // --- the written role responsibilities (task 1003732, criterion
425
+ // wa6-written-responsibilities). ONE source for the sentence each craft is
426
+ // answerable for; it reaches modules through the doorway because more than
427
+ // one module needs the same text — grading references it when judging
428
+ // role-shaped work, and the hall shows it to the person being judged — and
429
+ // modules never import each other. `responsibilityFor(discipline)` returns
430
+ // null for a craft with no statement (a module-contributed one, or an
431
+ // unclassified task); that is a legitimate answer, not an error.
432
+ // Plain entries rather than getters: the module is loaded eagerly at the
433
+ // top of this file (it costs nothing), so there is nothing left to defer.
434
+ responsibilityFor,
435
+ ROLE_RESPONSIBILITIES,
436
+
408
437
  // --- logging
409
438
  logger,
410
439
  };
package/src/modules.js CHANGED
@@ -28,6 +28,7 @@ const path = require('node:path');
28
28
  // until a module is moved into modules/ — so this is a no-op on the current tree.
29
29
  const loader = require('./module-loader/loader');
30
30
  const { resolveCoreRoot, resolveInstanceRoot } = require('./instance-config');
31
+ const { responsibilitiesFor } = require('./role-responsibilities');
31
32
 
32
33
  // ADR 0108 §1: the neutral starter ships WITH the core package; the instance
33
34
  // pack is host content. Both resolvers return the repo root today, so this is
@@ -250,10 +251,20 @@ function enabledDisciplines(set = modules()) {
250
251
 
251
252
  // CLIENT-SAFE projection — module flags are just booleans, all safe to expose.
252
253
  // The hall/status front-ends read this to hide UI sections for off modules.
254
+ //
255
+ // `responsibilities` rides here (task 1003732) because this projection is already
256
+ // injected into every served page as a global (serve-internal.injectBrandingGlobal),
257
+ // and it already carries the discipline roster the statements are keyed by — so the
258
+ // profile gets the owner's text with no new route, no fetch, and no second copy to
259
+ // drift. Scoped to the disciplines this instance actually offers: a craft the
260
+ // instance does not have must not appear in its UI, and a module-contributed craft
261
+ // has no statement by design (src/role-responsibilities.js says why).
253
262
  function clientModules(set = modules()) {
263
+ const disciplines = enabledDisciplines(set);
254
264
  return {
255
265
  enabled: knownModules().reduce((o, k) => { o[k] = set[k] === true; return o; }, {}),
256
- disciplines: enabledDisciplines(set),
266
+ disciplines,
267
+ responsibilities: responsibilitiesFor(disciplines),
257
268
  };
258
269
  }
259
270
 
@@ -0,0 +1,80 @@
1
+ // src/role-responsibilities.js
2
+ //
3
+ // WHAT. The one source of each craft's written responsibility — the sentence that
4
+ // says what a person holding that role is actually answerable for. Three consumers
5
+ // read this file and nothing else: the hall profile shows it to the person, the
6
+ // role pack under docs/packs/ teaches a session from it, and the grader references
7
+ // it when it judges role-shaped work.
8
+ //
9
+ // WHY IT IS ONE FILE. Criterion wa6-written-responsibilities (goal 1000095) is
10
+ // explicit that there is ONE source — "shown on the profile, injected into the
11
+ // pack, referenced by grading" — and the reason is not tidiness. The promise made
12
+ // to a builder on their profile and the text their session is actually held to
13
+ // must be the same text; two copies that drift mean the project judged someone
14
+ // against a standard it never showed them. The pack half is enforced rather than
15
+ // promised: the packs carry a GENERATED block written by
16
+ // scripts/gds/gen-role-responsibilities.js, and fitness.js fails CI when it drifts
17
+ // from this file (the gen-diagrams / gen-session-index pattern, ADR 0062 §8).
18
+ //
19
+ // THE TEXT IS THE OWNER'S, VERBATIM. Fixed by the area owner on 2026-09-08 and
20
+ // recorded in the criterion. Do not paraphrase, tighten, or "improve" these
21
+ // sentences — a role's responsibility is the owner's to write, and the whole point
22
+ // of a single source is that it says what they said. Per-project sub-specialities
23
+ // (idea 1000733) build on this later and are deliberately not modelled here.
24
+ //
25
+ // KERNEL FILE. On the fitness.js KERNEL_FILES roster: three different modules read
26
+ // it (hall-ui, grading) plus the core's own client projection, and modules never
27
+ // import each other, so shared vocabulary has to sit in the kernel. It imports
28
+ // nothing, which is what makes that placement free.
29
+ 'use strict';
30
+
31
+ // discipline key (tasks.discipline / builders.preferred_disciplines) → the owner's
32
+ // sentence. Keys mirror CORE_DISCIPLINES order (engineer | artist | ideator), the
33
+ // migration 176 enum order. A module-contributed discipline (ui-design → 'ui') has
34
+ // no statement here on purpose: a craft a module brings is that module's to
35
+ // describe, and inventing one for it would be exactly the paraphrase this file
36
+ // forbids. Consumers must therefore treat "no statement" as normal, not as an error.
37
+ const ROLE_RESPONSIBILITIES = Object.freeze({
38
+ engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
39
+ artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld.',
40
+ ideator: 'Make good ideas; be a philosopher / thought leader for the project; enable Engineers to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
41
+ });
42
+
43
+ // Provenance, carried with the text so every rendering can say where it came from
44
+ // rather than looking like a platform opinion someone typed.
45
+ const RESPONSIBILITIES_SOURCE = Object.freeze({
46
+ criterion: 'wa6-written-responsibilities',
47
+ goalId: '1000095',
48
+ authoredOn: '2026-09-08',
49
+ authoredBy: 'the area owner',
50
+ });
51
+
52
+ // The statement for one discipline, or null when the craft has none (a
53
+ // module-contributed discipline, an unclassified task, a stray value from an older
54
+ // row). Null is a legitimate answer every consumer must render as absence — never
55
+ // as a blank quote or a placeholder sentence.
56
+ function responsibilityFor(discipline) {
57
+ if (typeof discipline !== 'string') return null;
58
+ return ROLE_RESPONSIBILITIES[discipline.toLowerCase()] || null;
59
+ }
60
+
61
+ // The subset of statements an instance actually offers, so a client projection
62
+ // never ships a craft the instance does not have. Takes the discipline roster the
63
+ // caller already resolved (modules.enabledDisciplines()) rather than reaching for
64
+ // the module registry itself — that would make this file import domain and cost it
65
+ // its place on the kernel roster.
66
+ function responsibilitiesFor(disciplines) {
67
+ const out = {};
68
+ for (const d of Array.isArray(disciplines) ? disciplines : []) {
69
+ const text = responsibilityFor(d);
70
+ if (text) out[d] = text;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ module.exports = {
76
+ ROLE_RESPONSIBILITIES,
77
+ RESPONSIBILITIES_SOURCE,
78
+ responsibilityFor,
79
+ responsibilitiesFor,
80
+ };
@@ -46,6 +46,7 @@ const PUBLISHED_SURFACE = [
46
46
  'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
47
47
  'buildInfo', 'validateOrRespond', 'LIMITS', 'parseId', 'asyncHandler', 'corsPublicGet', // parseId added 1.8.0 (BV1.R78); asyncHandler + corsPublicGet added 1.12.0 (BV1.R86; lifecycle routes)
48
48
  'parsePagination', 'pageMeta', 'PAGINATION', // R14 (#2001, 1.17.0): shared pagination contract for module list routes (ADR 0119)
49
+ 'responsibilityFor', 'ROLE_RESPONSIBILITIES', // added by task 1003732 (ADR 0275; criterion wa6-written-responsibilities): the ONE source for the sentence each craft is answerable for. It rides the doorway because two modules need the same text — grading judges role-shaped work against it, the hall shows it to the person being judged — and modules never import each other. Declared here deliberately: this list is what makes adding to the doorway a decision rather than a side effect
49
50
  'logger',
50
51
  ];
51
52
 
@@ -0,0 +1,187 @@
1
+ // tests/role_responsibilities.mjs
2
+ //
3
+ // task 1003732 (goal 1000095, criterion wa6-written-responsibilities) — the three
4
+ // written role responsibilities land as ONE source with three consumers.
5
+ //
6
+ // WHAT IS WORTH PINNING HERE. The criterion's whole content is "one source, shown
7
+ // on the profile, injected into the pack, referenced by grading". So the failure to
8
+ // guard against is not a typo — it is a SECOND COPY. If the packs, the hall and the
9
+ // grader can each hold their own wording, the project can show a builder one
10
+ // standard and judge them by another, which is the specific unfairness the single
11
+ // source exists to prevent. Every test below is a leg of that.
12
+ //
13
+ // The text itself is the owner's, verbatim (fixed 2026-09-08). It is asserted here
14
+ // character-for-character on purpose: a well-meaning tidy-up of someone else's
15
+ // authored sentence is exactly the drift this task forbids, and a test is the only
16
+ // thing that makes "do not paraphrase" survive contact with a future editor.
17
+ //
18
+ // Run: node tests/role_responsibilities.mjs
19
+
20
+ import { strict as assert } from 'node:assert';
21
+ import { createRequire } from 'node:module';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const src = require(path.join(ROOT, 'src', 'role-responsibilities.js'));
29
+ const modules = require(path.join(ROOT, 'src', 'modules.js'));
30
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
31
+ const gen = require(path.join(ROOT, 'scripts', 'gds', 'gen-role-responsibilities.js'));
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+ function test(name, fn) {
36
+ try { fn(); passed++; console.log(` ok ${name}`); }
37
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
38
+ }
39
+
40
+ // --- the source ---------------------------------------------------------
41
+
42
+ // The owner's sentences, verbatim from criterion wa6-written-responsibilities.
43
+ // Duplicated here deliberately — a test that read the value it is checking would
44
+ // assert nothing. This copy is the fixture; the source file is the subject.
45
+ const OWNER_TEXT = {
46
+ engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
47
+ artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld.',
48
+ ideator: 'Make good ideas; be a philosopher / thought leader for the project; enable Engineers to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
49
+ };
50
+
51
+ test('the three statements are the owner\'s text, character for character', () => {
52
+ assert.deepEqual({ ...src.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
53
+ });
54
+
55
+ test('exactly the three CORE crafts carry a statement — no more, no fewer', () => {
56
+ assert.deepEqual(Object.keys(src.ROLE_RESPONSIBILITIES), ['engineer', 'artist', 'ideator']);
57
+ // Governor is deferred (ADR 0274) and `ui` is module-contributed (ADR 0272):
58
+ // inventing a sentence for either is the paraphrase this task forbids.
59
+ assert.equal(src.responsibilityFor('ui'), null);
60
+ assert.equal(src.responsibilityFor('governor'), null);
61
+ });
62
+
63
+ test('the map is frozen — a consumer cannot edit the standard at runtime', () => {
64
+ assert.ok(Object.isFrozen(src.ROLE_RESPONSIBILITIES));
65
+ });
66
+
67
+ test('responsibilityFor: case-insensitive, and absence is null not a placeholder', () => {
68
+ assert.equal(src.responsibilityFor('ENGINEER'), OWNER_TEXT.engineer);
69
+ for (const bad of [null, undefined, '', 'unclassified', 42, {}]) {
70
+ assert.equal(src.responsibilityFor(bad), null, `${JSON.stringify(bad)} must be null`);
71
+ }
72
+ });
73
+
74
+ test('responsibilitiesFor scopes to the disciplines an instance offers', () => {
75
+ assert.deepEqual(src.responsibilitiesFor(['artist', 'ui']), { artist: OWNER_TEXT.artist });
76
+ assert.deepEqual(src.responsibilitiesFor([]), {});
77
+ assert.deepEqual(src.responsibilitiesFor(null), {});
78
+ });
79
+
80
+ // --- consumer 1: the hall (via the injected client projection) ----------
81
+
82
+ test('clientModules carries the statements for the offered crafts only', () => {
83
+ const c = modules.clientModules();
84
+ for (const d of Object.keys(c.responsibilities)) {
85
+ assert.ok(c.disciplines.includes(d), `${d} is offered a statement but is not an offered discipline`);
86
+ assert.equal(c.responsibilities[d], OWNER_TEXT[d], `${d}: the projection must not reword the source`);
87
+ }
88
+ // `ui` is offered by the ui-design module but has no statement — the projection
89
+ // must carry the craft without inventing a standard for it.
90
+ if (c.disciplines.includes('ui')) assert.equal(c.responsibilities.ui, undefined);
91
+ });
92
+
93
+ // --- consumer 2: the grader (via the module doorway) --------------------
94
+
95
+ test('the doorway exposes it, so a module never deep-requires the source', () => {
96
+ assert.equal(api.responsibilityFor('artist'), OWNER_TEXT.artist);
97
+ assert.deepEqual({ ...api.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
98
+ });
99
+
100
+ test('the grader prompt states the standard for a craft, and stays silent without one', () => {
101
+ const { buildPrompt } = require(path.join(ROOT, 'modules', 'grading', 'grader-prompt.js'));
102
+ const build = (discipline) => buildPrompt({
103
+ task: { id: 1, title: 't', kind: 'feature', discipline },
104
+ valueSummary: 'v', changedFiles: ['a.js'], diff: 'x',
105
+ });
106
+ assert.ok(build('artist').includes(OWNER_TEXT.artist), 'the artist statement must reach the prompt verbatim');
107
+ // A craft with no written standard must not get an invented one — a grader told
108
+ // "this role has no standard" would supply its own.
109
+ for (const d of ['ui', 'unclassified', undefined]) {
110
+ assert.ok(!build(d).includes('What this craft is answerable for'), `${d} must add no responsibility line`);
111
+ }
112
+ });
113
+
114
+ // --- consumer 3: the packs (generated, so they cannot drift) ------------
115
+
116
+ test('every pack the registry names carries the block, matching the source', () => {
117
+ const { writes, warnings } = gen.plan();
118
+ assert.deepEqual(writes.map((w) => w.rel), [], `a pack block is stale: ${writes.map((w) => w.rel).join(', ')}`);
119
+ assert.deepEqual(warnings, [], `every registered pack must carry the marker pair: ${warnings.join(' | ')}`);
120
+ });
121
+
122
+ test('the block carries the statement verbatim and says it is generated', () => {
123
+ for (const t of gen.packTargets()) {
124
+ const text = fs.readFileSync(t.abs, 'utf8');
125
+ const expected = OWNER_TEXT[t.discipline];
126
+ if (!expected) continue;
127
+ assert.ok(text.includes(expected), `${t.rel} must quote the ${t.discipline} statement verbatim`);
128
+ assert.ok(text.includes(gen.BEGIN) && text.includes(gen.END), `${t.rel} must keep the marker pair`);
129
+ assert.ok(/do not hand-edit/i.test(text), `${t.rel} must warn that the block is generated`);
130
+ }
131
+ });
132
+
133
+ test('the generator is idempotent and preserves a CRLF pack\'s line endings', () => {
134
+ // The packs are .md: CRLF on a Windows checkout, LF in CI. A generator that
135
+ // always wrote '\n' would make the committed bytes platform-dependent, so the
136
+ // freshness gate would be red on one checkout and green on the other.
137
+ const block = gen.blockFor('artist');
138
+ const crlf = `head\r\n${gen.BEGIN}\r\nstale\r\n${gen.END}\r\ntail\r\n`;
139
+ const out = gen.inject(crlf, block);
140
+ assert.ok(!/[^\r]\n/.test(out), 'a CRLF file must stay pure CRLF');
141
+ assert.equal(gen.inject(out, block), out, 'a second pass must change nothing');
142
+
143
+ const lf = `head\n${gen.BEGIN}\nstale\n${gen.END}\ntail\n`;
144
+ const outLf = gen.inject(lf, block);
145
+ assert.ok(!outLf.includes('\r'), 'an LF file must stay pure LF');
146
+ assert.equal(gen.inject(outLf, block), outLf, 'a second pass must change nothing');
147
+ });
148
+
149
+ test('a pack with no markers is reported, never silently skipped', () => {
150
+ assert.equal(gen.inject('no markers here', gen.blockFor('artist')), null);
151
+ });
152
+
153
+ // --- the whole point: there is no second copy ---------------------------
154
+
155
+ test('no consumer hardcodes a statement — the packs\' generated blocks are the only copies', () => {
156
+ // Anything that repeats a statement outside the source, its generated blocks, the
157
+ // criterion record, or this fixture is a second copy that can drift.
158
+ const ALLOWED = new Set([
159
+ 'src/role-responsibilities.js', // the source
160
+ 'docs/packs/engineer.md', // generated blocks
161
+ 'docs/packs/artist.md',
162
+ 'docs/packs/ideator.md',
163
+ 'tests/role_responsibilities.mjs', // this fixture
164
+ ]);
165
+ const scan = ['src', 'scripts', 'modules', 'tests', 'docs/packs'];
166
+ const offenders = [];
167
+ const walk = (dir) => {
168
+ let entries = [];
169
+ try { entries = fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true }); } catch { return; }
170
+ for (const e of entries) {
171
+ const rel = `${dir}/${e.name}`;
172
+ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(rel); continue; }
173
+ if (!/\.(js|mjs|md|json|html)$/.test(e.name)) continue;
174
+ if (ALLOWED.has(rel)) continue;
175
+ let text;
176
+ try { text = fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { continue; }
177
+ for (const [craft, sentence] of Object.entries(OWNER_TEXT)) {
178
+ if (text.includes(sentence)) offenders.push(`${rel} repeats the ${craft} statement`);
179
+ }
180
+ }
181
+ };
182
+ for (const d of scan) walk(d);
183
+ assert.deepEqual(offenders, [], `a second copy can drift from the source:\n ${offenders.join('\n ')}`);
184
+ });
185
+
186
+ console.log(`\nrole_responsibilities: ${passed} passed, ${failed} failed`);
187
+ if (failed > 0) process.exit(1);