@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.
- package/.bongos-core.json +46 -26
- package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +50 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +24 -23
- package/docs/copy-registry.json +39 -30
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/packs/artist.md +6 -2
- package/docs/packs/engineer.md +10 -0
- package/docs/packs/ideator.md +10 -0
- package/modules/grading/grader-prompt.js +14 -0
- package/modules/hall-ui/public/profile.css +17 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +4 -0
- package/scripts/gds/gen-role-responsibilities.js +134 -0
- package/scripts/gds/role-pack-guard.js +21 -0
- package/src/module-api.js +30 -1
- package/src/modules.js +12 -1
- package/src/role-responsibilities.js +80 -0
- package/tests/module_api.mjs +1 -0
- package/tests/role_responsibilities.mjs +187 -0
|
@@ -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.
|
|
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
|
|
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
|
+
};
|
package/tests/module_api.mjs
CHANGED
|
@@ -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);
|