@bongos/core 1.24.7 → 1.24.9
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 +115 -50
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -1
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +1 -1
- package/docs/api/openapi.json +267 -7
- package/docs/api-reference.md +7 -3
- package/docs/architecture.md +2 -2
- package/docs/copy-inventory.md +38 -13
- package/docs/copy-registry.json +271 -15
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +3 -3
- package/docs/page-inventory.json +40 -4
- package/docs/page-readings.json +64 -4
- package/migrations/core_287_module_prices_cash_only.sql +24 -0
- package/modules/government/decision-board.js +5 -0
- package/modules/government/decision-rules.js +3 -0
- package/modules/government/routes/government.js +2 -0
- package/modules/hall-ui/public/market-watch-rules-lib.js +207 -0
- package/modules/hall-ui/public/market-watch-rules.css +68 -0
- package/modules/hall-ui/public/market-watch-rules.html +54 -0
- package/modules/hall-ui/public/market-watch-rules.js +129 -0
- package/modules/hall-ui/public/market-watch-rules.states.json +17 -0
- package/modules/hall-ui/public/modules.js +5 -0
- package/modules/market-watch/matter-acts.js +59 -0
- package/modules/market-watch/module.json +5 -4
- package/modules/market-watch/proposals.js +214 -0
- package/modules/market-watch/research.js +49 -8
- package/modules/market-watch/routes/rules.js +202 -0
- package/modules/market-watch/rules-logic.js +264 -0
- package/modules/market-watch/rules-view.js +165 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/government-walk.js +18 -2
- package/scripts/bongos/module-store-cli.js +21 -21
- package/scripts/bongos/module.js +8 -8
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/src/bongos/module-listing.js +47 -20
- package/src/bongos/module-scope-map.js +2 -0
- package/src/bongos/module-store.js +20 -12
- package/src/bongos/routes/modules.js +31 -21
- package/src/bongos/serve-internal.js +2 -1
- package/src/module-api.js +1 -1
- package/tests/government_act_alone_buildings.mjs +3 -0
- package/tests/government_decision_rules.mjs +4 -1
- package/tests/governor_matter_acts.mjs +5 -1
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/market_watch_module.mjs +1 -0
- package/tests/market_watch_rules.mjs +496 -0
- package/tests/market_watch_rules_db.mjs +133 -0
- package/tests/module_store_cli.mjs +14 -3
- package/tests/module_store_listing.mjs +65 -14
- package/tests/module_store_price.mjs +97 -26
- package/tests/module_store_registry_migration.mjs +11 -0
package/release-notes.json
CHANGED
|
@@ -10877,5 +10877,17 @@
|
|
|
10877
10877
|
"id": "1005003",
|
|
10878
10878
|
"text": "The automatic tweak limit is now 200 rounds a month, and a project owner can change it, for the whole project and separately for each builder rank, on the project settings page."
|
|
10879
10879
|
}
|
|
10880
|
+
],
|
|
10881
|
+
"1.24.8": [
|
|
10882
|
+
{
|
|
10883
|
+
"id": "1004941",
|
|
10884
|
+
"text": "Every member can now read the Market Watch rules step by step, propose a change with a reason, and the owner (or the government, by vote) approves it into a new rules version that the next weekly run uses."
|
|
10885
|
+
}
|
|
10886
|
+
],
|
|
10887
|
+
"1.24.9": [
|
|
10888
|
+
{
|
|
10889
|
+
"id": "1004989",
|
|
10890
|
+
"text": "Modules can now only be priced in cash: credits can no longer price or buy one, and modules that still have an old credit price stay listed as \"price coming\" while their authors are asked to set a cash price."
|
|
10891
|
+
}
|
|
10880
10892
|
]
|
|
10881
10893
|
}
|
|
@@ -84,6 +84,7 @@ const REQUESTS = Object.freeze({
|
|
|
84
84
|
'PATCH /project-settings/tweak-autopickup': ['PATCH', '/project-settings/tweak-autopickup', { mode: 'sometimes' }],
|
|
85
85
|
'PATCH /project-settings/spend-cap': ['PATCH', '/project-settings/spend-cap', { usd: 'lots' }],
|
|
86
86
|
'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
|
|
87
|
+
'POST /market-watch/proposals/:id/decide': ['POST', `/market-watch/proposals/${BAD}/decide`, { decision: 'approve' }],
|
|
87
88
|
'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
|
|
88
89
|
'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
|
|
89
90
|
'POST /security/reports/:id/fix': ['POST', `/security/reports/${BAD}/fix`, {}],
|
|
@@ -123,6 +124,7 @@ const MATTER_PLAN = Object.freeze([
|
|
|
123
124
|
['release', 'Approve override request 5', { type: 'approve_override', request_id: 5 }, 'request 5 approved'],
|
|
124
125
|
['review', 'Sign off criterion 9', { type: 'satisfy_criterion', criterion_id: 9 }, 'criterion 9 satisfied'],
|
|
125
126
|
['treasury', 'Dee gets $50 a month', { type: 'set_budget', builder_id: 45, monthly_usd: 50 }, 'Dee\'s limit is $50'],
|
|
127
|
+
['rules', 'Market Watch: + bounty boards as a search', { type: 'market_watch_proposal', proposal_id: 12 }, 'proposal 12 approved'],
|
|
126
128
|
]);
|
|
127
129
|
|
|
128
130
|
// Every router the walk drives, mounted as the server mounts them.
|
|
@@ -145,6 +147,8 @@ const ROUTERS = Object.freeze([
|
|
|
145
147
|
// the new machinery's other doors: a blocker and its ask, a builder's settings
|
|
146
148
|
'modules/ideas/routes/blockers.js',
|
|
147
149
|
'modules/builder-settings/routes/settings.js',
|
|
150
|
+
// task 1004941: the owner's decide route (a Rules act) and the Rules matter act
|
|
151
|
+
'modules/market-watch/routes/rules.js',
|
|
148
152
|
]);
|
|
149
153
|
|
|
150
154
|
// ── the in-memory government store ──────────────────────────────────────────
|
|
@@ -633,7 +637,17 @@ async function runWalk() {
|
|
|
633
637
|
projectSettings.setSetting = async (k, v) => { settings.set(k, String(v)); return { key: k, value: String(v) }; };
|
|
634
638
|
|
|
635
639
|
// What passed matters write into other modules, recorded.
|
|
636
|
-
const effects = { fence: null, allowedGoals: [], overrides: [], criteria: new Map([['9', false]]), budgets: new Map() };
|
|
640
|
+
const effects = { fence: null, allowedGoals: [], overrides: [], criteria: new Map([['9', false]]), budgets: new Map(), mwApproved: [] };
|
|
641
|
+
// task 1004941: a passed Rules matter approves a Market Watch proposal through the
|
|
642
|
+
// module's own decideProposal; here it is recorded instead of written.
|
|
643
|
+
const mwActs = req('modules/market-watch/matter-acts.js');
|
|
644
|
+
const mwProposals = req('modules/market-watch/proposals.js');
|
|
645
|
+
mwActs.runtime.project = () => 'https://walk.example';
|
|
646
|
+
mwActs.runtime.withTx = async (fn) => fn(null);
|
|
647
|
+
mwProposals.decideProposal = async ({ id, decision }) => {
|
|
648
|
+
if (decision === 'approve') effects.mwApproved.push(String(id));
|
|
649
|
+
return { ok: true, status: 'applied', applied: true };
|
|
650
|
+
};
|
|
637
651
|
autoDb.setFenceEnabled = async ({ enabled }) => { effects.fence = enabled; return { enabled }; };
|
|
638
652
|
autoDb.allowGoal = async ({ goalId }) => { effects.allowedGoals.push(String(goalId)); return { goal_id: goalId }; };
|
|
639
653
|
lifeDb.decideOverrideRequest = async ({ requestId, decision }) => { effects.overrides.push([String(requestId), decision]); return {}; };
|
|
@@ -943,6 +957,7 @@ async function runWalk() {
|
|
|
943
957
|
section('M Matters');
|
|
944
958
|
const SITTINGS = {
|
|
945
959
|
people: 'consent', automation: 'majority', release: 'unanimous', review: 'signoff', treasury: 'first',
|
|
960
|
+
rules: 'majority',
|
|
946
961
|
};
|
|
947
962
|
await check('Each acting building is put to the council by one passed amendment', Object.entries(SITTINGS).map(([a, r]) => `${a} ${r}`).join(', '), async () => {
|
|
948
963
|
const out = await setRules(Object.entries(SITTINGS).map(([area, rule]) => ({ area, decided_by: 'rank:council', rule })));
|
|
@@ -971,12 +986,13 @@ async function runWalk() {
|
|
|
971
986
|
'release.approve_override': () => effects.overrides.some(([id, d]) => id === '5' && d === 'approve'),
|
|
972
987
|
'review.satisfy_criterion': () => effects.criteria.get('9') === true,
|
|
973
988
|
'treasury.set_budget': () => effects.budgets.get('45') === 50,
|
|
989
|
+
'rules.market_watch_proposal': () => effects.mwApproved.includes('12'),
|
|
974
990
|
};
|
|
975
991
|
const MATTERS = MATTER_PLAN.map(([area, title, action, effect]) => [area, title, action, effect, HAPPENED[`${area}.${action.type}`]]);
|
|
976
992
|
// How each building's sitting closes as Ben then Cy say yes, under its rule: the
|
|
977
993
|
// two-member consent, majority and unanimous sittings need both; first yes and
|
|
978
994
|
// sign-off (by someone who did not propose it) close on the first.
|
|
979
|
-
const CLOSES = { people: 'open, passed', automation: 'open, passed', release: 'open, passed', review: 'passed', treasury: 'passed' };
|
|
995
|
+
const CLOSES = { people: 'open, passed', automation: 'open, passed', release: 'open, passed', review: 'passed', treasury: 'passed', rules: 'open, passed' };
|
|
980
996
|
await check('Someone the building\'s row does not seat cannot vote on its matter', '403 not_board_member', async () => {
|
|
981
997
|
const r = await call(FOUNDER, 'POST', '/government/board/matters', { area: 'automation', title: 'Who may vote here?' });
|
|
982
998
|
const itemId = r.json && r.json.item && r.json.item.id;
|
|
@@ -63,13 +63,17 @@ function storeError(res) {
|
|
|
63
63
|
// bongos module store — what the store lists (task 1003811)
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
|
-
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--
|
|
66
|
+
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--json]';
|
|
67
|
+
|
|
68
|
+
// Credits never price or buy a module (task 1004989, ADR 0385 decision 2).
|
|
69
|
+
const NO_CREDIT_PRICE = 'credits do not price modules: use cents';
|
|
67
70
|
|
|
68
71
|
// The query GET /store/modules takes, from argv. Returns { query } or { error }.
|
|
69
72
|
// The server checks the values too; this only refuses what is not a value at all.
|
|
70
73
|
function storeQuery(args) {
|
|
74
|
+
if (args.includes('--max-credits')) return { error: NO_CREDIT_PRICE };
|
|
71
75
|
const params = [];
|
|
72
|
-
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents']
|
|
76
|
+
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents']]) {
|
|
73
77
|
const i = args.indexOf(flag);
|
|
74
78
|
if (i === -1) continue;
|
|
75
79
|
const v = args[i + 1];
|
|
@@ -79,12 +83,12 @@ function storeQuery(args) {
|
|
|
79
83
|
return { query: params.length ? `?${params.join('&')}` : '' };
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
// A cash price, "free", or "price coming" for a module that still carries only an
|
|
87
|
+
// old credit price. Never a credit amount.
|
|
82
88
|
function formatPrice(price) {
|
|
83
89
|
if (!price || price.free) return 'free';
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (price.credits != null) parts.push(`${price.credits} credits`);
|
|
87
|
-
return parts.join(' or ') || 'free';
|
|
90
|
+
if (price.cents != null) return `$${(price.cents / 100).toFixed(2)}`;
|
|
91
|
+
return 'price coming';
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
function formatPlace(category) {
|
|
@@ -104,7 +108,7 @@ function formatStore(body) {
|
|
|
104
108
|
out.push(` sorted by ${part ? part.title : body.sort}`);
|
|
105
109
|
}
|
|
106
110
|
const max = body.max_price;
|
|
107
|
-
if (max) out.push(` at most
|
|
111
|
+
if (max && max.cents != null) out.push(` at most $${(max.cents / 100).toFixed(2)}`);
|
|
108
112
|
out.push('');
|
|
109
113
|
const listed = body.listed || [];
|
|
110
114
|
if (!listed.length) out.push(' Nothing is listed yet.');
|
|
@@ -141,22 +145,18 @@ async function cmdStore(args, { log = console.log, errlog = console.error, reque
|
|
|
141
145
|
// bongos module price — the author sets a module's price (task 1004894)
|
|
142
146
|
// ---------------------------------------------------------------------------
|
|
143
147
|
|
|
144
|
-
const PRICE_USAGE = 'usage: bongos module price <key>
|
|
148
|
+
const PRICE_USAGE = 'usage: bongos module price <key> --cents N (0 makes it free)';
|
|
145
149
|
|
|
146
|
-
// argv -> { key, cents
|
|
150
|
+
// argv -> { key, cents } or { error }. Pure.
|
|
147
151
|
function priceArgs(args) {
|
|
148
152
|
const key = args[0];
|
|
149
153
|
if (!key || key.startsWith('--') || !KEY_RE.test(key)) return { error: 'name the module: a lowercase kebab-case key' };
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
out[field] = Number(v);
|
|
157
|
-
}
|
|
158
|
-
if (out.cents === null && out.credits === null) return { error: 'give --cents, --credits or both' };
|
|
159
|
-
return out;
|
|
154
|
+
if (args.includes('--credits')) return { error: NO_CREDIT_PRICE };
|
|
155
|
+
const i = args.indexOf('--cents');
|
|
156
|
+
if (i === -1) return { error: 'give --cents' };
|
|
157
|
+
const v = args[i + 1];
|
|
158
|
+
if (!v || !/^\d{1,9}$/.test(v)) return { error: '--cents needs a whole number, 0 or more' };
|
|
159
|
+
return { key, cents: Number(v) };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
async function cmdPrice(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
@@ -166,10 +166,10 @@ async function cmdPrice(args, { log = console.log, errlog = console.error, reque
|
|
|
166
166
|
try { call = request || storeRequest(); }
|
|
167
167
|
catch (e) { errlog(`bongos module price: ${e.message}`); return 1; }
|
|
168
168
|
let res;
|
|
169
|
-
try { res = await call('POST', `/store/modules/${encodeURIComponent(p.key)}/price`, { json: { cents: p.cents
|
|
169
|
+
try { res = await call('POST', `/store/modules/${encodeURIComponent(p.key)}/price`, { json: { cents: p.cents } }); }
|
|
170
170
|
catch (e) { errlog(`bongos module price: the store could not be reached — ${e.message}`); return 1; }
|
|
171
171
|
if (!res.ok) { errlog(`bongos module price: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
172
|
-
const withFree = (p) => ({ ...p, free: p.cents === 0
|
|
172
|
+
const withFree = (p) => ({ ...p, free: p.cents === 0 });
|
|
173
173
|
const shown = formatPrice(withFree(res.data.price));
|
|
174
174
|
const was = res.data.previous ? formatPrice(withFree(res.data.previous)) : 'free (no price set)';
|
|
175
175
|
log(`✓ "${p.key}" now costs ${shown} (was ${was}).`);
|
package/scripts/bongos/module.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// model as the hall's Modules tab (src/module-loader/catalog.js), so the two
|
|
32
32
|
// surfaces cannot disagree about who built what.
|
|
33
33
|
//
|
|
34
|
-
// bongos module store [--sort <part>] [--max-cents N] [--
|
|
34
|
+
// bongos module store [--sort <part>] [--max-cents N] [--json]
|
|
35
35
|
// Task 1003811 (criterion wa5-curated-listing) — the STORE from the terminal:
|
|
36
36
|
// what the catalog lists, in its order. It asks GET /store/modules, the same
|
|
37
37
|
// read the hall's "In the store" group makes, and never filters, ranks or caps
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
// sub-category live in one place (src/bongos/module-listing.js), so the hall
|
|
40
40
|
// and the terminal cannot disagree about which modules are the best.
|
|
41
41
|
//
|
|
42
|
-
// bongos module price <key>
|
|
42
|
+
// bongos module price <key> --cents N (credits never price a module)
|
|
43
43
|
// Task 1004894 — the module's author sets its price in the store, in money
|
|
44
|
-
// (cents)
|
|
44
|
+
// (cents); 0 makes it free. A credit price is refused (task 1004989). The store keeps every
|
|
45
45
|
// change and shows the newest. A paid module is listed as not yet buyable until
|
|
46
46
|
// buying arrives (task 1003788).
|
|
47
47
|
//
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
// bongos module new <key> scaffold modules/<key>/
|
|
73
73
|
// bongos module new <key> --force overwrite an existing modules/<key>/
|
|
74
74
|
// bongos module list [--catalog] browse the module catalog (author + origin credit)
|
|
75
|
-
// bongos module store [--sort <part>] [--max-cents N]
|
|
75
|
+
// bongos module store [--sort <part>] [--max-cents N] what the store lists, best first
|
|
76
76
|
// bongos upgrade [--no-store] pre-check enabled modules vs the core version + list store updates
|
|
77
77
|
// bongos module check <key> [--sign-off "Name <email>"] AGPL/publish pre-check + DCO sign-off
|
|
78
78
|
// bongos module install <key> [--version X.Y.Z] acquire from the store, verify, place (not enabled)
|
|
@@ -968,7 +968,7 @@ async function cmdPublish(args, {
|
|
|
968
968
|
log = console.log, errlog = console.error,
|
|
969
969
|
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync, priceRequest,
|
|
970
970
|
} = {}) {
|
|
971
|
-
const PUBLISH_USAGE = 'usage: bongos module publish <key> [--dry-run] [--out <dir>] [--cents N]
|
|
971
|
+
const PUBLISH_USAGE = 'usage: bongos module publish <key> [--dry-run] [--out <dir>] [--cents N]';
|
|
972
972
|
const VALUED = new Set(['--out', '--cents', '--credits']);
|
|
973
973
|
const key = args.find((a, i) => !a.startsWith('-') && !VALUED.has(args[i - 1]));
|
|
974
974
|
if (!key) {
|
|
@@ -1335,9 +1335,9 @@ function printHelp(out = console.log) {
|
|
|
1335
1335
|
out(' bongos module new <key> Scaffold modules/<key>/ (manifest + skeleton + sample route + HOWTO.md + CLAUDE.md)');
|
|
1336
1336
|
out(' bongos module list [--catalog] [--search <text>] [--json]');
|
|
1337
1337
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1338
|
-
out(' bongos module store [--sort <part>] [--max-cents N] [--
|
|
1338
|
+
out(' bongos module store [--sort <part>] [--max-cents N] [--json]');
|
|
1339
1339
|
out(' What the store lists, best first: above the quality floor, ranked, ten per sub-category');
|
|
1340
|
-
out(' bongos module price <key>
|
|
1340
|
+
out(' bongos module price <key> --cents N');
|
|
1341
1341
|
out(' Set your module\'s price in the store (0 in either makes it free)');
|
|
1342
1342
|
out(' bongos module remove <key> Take a store module off this project (switch it off first; its folder is kept aside)');
|
|
1343
1343
|
out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
|
|
@@ -1346,7 +1346,7 @@ function printHelp(out = console.log) {
|
|
|
1346
1346
|
out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
|
|
1347
1347
|
out(' bongos module submit <key> [--sign-off "Name <email>"]');
|
|
1348
1348
|
out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
|
|
1349
|
-
out(' bongos module publish <key> [--dry-run] [--out <dir>] [--cents N]
|
|
1349
|
+
out(' bongos module publish <key> [--dry-run] [--out <dir>] [--cents N]');
|
|
1350
1350
|
out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
|
|
1351
1351
|
out(' bongos module install <key> [--version X.Y.Z]');
|
|
1352
1352
|
out(' Acquire a module from the store, verify it and place it under modules/ (not enabled)');
|
|
@@ -93,6 +93,9 @@ const INTEGRATION = new Set([
|
|
|
93
93
|
// idempotent and a resumed run unable to record an item twice. DB-free sibling:
|
|
94
94
|
// market_watch_data.
|
|
95
95
|
'market_watch_data_db',
|
|
96
|
+
// task 1004941: a rules proposal round-trips to a new version the weekly run's loader
|
|
97
|
+
// reads, on real SQL (the one-active index, the CHECKs). DB-free sibling: market_watch_rules.
|
|
98
|
+
'market_watch_rules_db',
|
|
96
99
|
// task 1004940: the weekly ask's SQL store — once per week by constraint, one pooled
|
|
97
100
|
// proposal per similar answer, the project guard on an answer. DB-free sibling:
|
|
98
101
|
// market_watch_ask.
|
|
@@ -135,39 +135,57 @@ function sortByPart(entries, part) {
|
|
|
135
135
|
// can both check in one sentence:
|
|
136
136
|
//
|
|
137
137
|
// highest overall score first; between equal scores the cheaper module first
|
|
138
|
-
// (free first, then the lowest cash price,
|
|
139
|
-
// the key.
|
|
138
|
+
// (free first, then the lowest cash price, a module whose cash price is still
|
|
139
|
+
// coming last); then the key.
|
|
140
140
|
//
|
|
141
141
|
// Price never buys a better place: it only orders modules the score could not tell
|
|
142
142
|
// apart (price is never an input to the score, ADR 0343 D6). A buyer may also set a
|
|
143
143
|
// maximum price, which leaves out what costs more — a filter, not a ranking.
|
|
144
144
|
const RANKING = 'Highest overall score first; between equal scores, free modules and then the cheapest come first.';
|
|
145
145
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
146
|
+
// CREDITS NEVER PRICE A MODULE (task 1004989, ADR 0385 decision 2). Credits are a
|
|
147
|
+
// contribution meter: a credit price would make them stored value. New price rows
|
|
148
|
+
// carry cents only (core_287 refuses price_credits). An OLD row may still carry a
|
|
149
|
+
// credit price; the owner's ruling (2026-10-29) is that such a module stays
|
|
150
|
+
// VISIBLE with a "price coming" note and can NOT be bought with credits, while
|
|
151
|
+
// its author is asked to set a cash price.
|
|
152
|
+
//
|
|
153
|
+
// A module's current price row as the catalog shows it: { cents, free, coming }.
|
|
154
|
+
// Never a credit amount. No row is free (isFreePrice, module-store.js); a cash
|
|
155
|
+
// price of 0 is free. An old row that offered it at 0 credits stays free, as it
|
|
156
|
+
// was: its author offered it at nothing, and no credit moves to have it. An old row whose
|
|
157
|
+
// only price was a positive number of credits is `coming` — no cash price yet,
|
|
158
|
+
// so not free and not buyable. Pure.
|
|
149
159
|
function priceOf(row) {
|
|
150
160
|
const cents = row && row.price_cents != null ? Number(row.price_cents) : null;
|
|
151
|
-
const
|
|
152
|
-
|
|
161
|
+
const legacyCredits = row && row.price_credits != null ? Number(row.price_credits) : null;
|
|
162
|
+
const free = !row || cents === 0 || legacyCredits === 0;
|
|
163
|
+
return { cents, free, coming: !free && cents == null };
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
// What the author of a "price coming" module is asked (task 1004989). One string,
|
|
167
|
+
// so the listing read, the CLI and the hall say the same thing.
|
|
168
|
+
const PRICE_COMING_PROMPT = 'This module still has a price in credits, and credits can no longer price or buy a module. '
|
|
169
|
+
+ 'It stays listed with "price coming" until you set a cash price: bongos module price <key> --cents N (0 makes it free).';
|
|
170
|
+
|
|
171
|
+
// Whether a module can be bought today. Buying arrives with task 1003788; until
|
|
172
|
+
// then only a free module can be had, and a paid one is listed with its price and
|
|
173
|
+
// marked not yet buyable (owner, 2026-10-26). Flip BUYING_OPEN when the buy action
|
|
174
|
+
// ships. A module whose cash price is still coming stays unbuyable even then:
|
|
175
|
+
// there is nothing to pay in but credits, and credits do not buy. Pure.
|
|
159
176
|
const BUYING_OPEN = false;
|
|
160
177
|
function buyableNow(price) {
|
|
161
|
-
return !!(price && (price.free || BUYING_OPEN));
|
|
178
|
+
return !!(price && (price.free || (BUYING_OPEN && price.cents != null)));
|
|
162
179
|
}
|
|
163
180
|
|
|
164
|
-
// Within a buyer's maximum ({ cents
|
|
165
|
-
// always is; a priced one is when
|
|
181
|
+
// Within a buyer's maximum cash price ({ cents }, or null for no cap): a free
|
|
182
|
+
// module always is; a priced one is when its cash price is at or under the cap.
|
|
183
|
+
// A module whose price is still coming has no cash price to compare, so a cap
|
|
184
|
+
// leaves it out. Pure.
|
|
166
185
|
function withinMaxPrice(price, max) {
|
|
167
|
-
if (!max ||
|
|
186
|
+
if (!max || max.cents == null) return true;
|
|
168
187
|
if (price.free) return true;
|
|
169
|
-
return
|
|
170
|
-
|| (max.credits != null && price.credits != null && price.credits <= max.credits);
|
|
188
|
+
return price.cents != null && price.cents <= max.cents;
|
|
171
189
|
}
|
|
172
190
|
|
|
173
191
|
// The default order of `listed`, per RANKING. Returns a new array. Pure.
|
|
@@ -179,7 +197,6 @@ function rankListed(entries) {
|
|
|
179
197
|
return ((b.overall ?? -1) - (a.overall ?? -1))
|
|
180
198
|
|| (Number(pb.free) - Number(pa.free))
|
|
181
199
|
|| (cheap(pa.cents) - cheap(pb.cents))
|
|
182
|
-
|| (cheap(pa.credits) - cheap(pb.credits))
|
|
183
200
|
|| a.key.localeCompare(b.key);
|
|
184
201
|
});
|
|
185
202
|
}
|
|
@@ -292,8 +309,8 @@ async function buildCatalog({ db, floor = QUALITY_FLOOR, sort = null, maxPrice =
|
|
|
292
309
|
signals.get(k).push(s);
|
|
293
310
|
}
|
|
294
311
|
}
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
// A cash cap only (task 1004989): credits are never a price, so never a cap.
|
|
313
|
+
const max = maxPrice && maxPrice.cents != null ? { cents: maxPrice.cents } : null;
|
|
297
314
|
const cats = await readModuleCategories(newest.map((r) => r.module_key), { db: pool });
|
|
298
315
|
const out = { floor, cap: CAP_PER_SUBCATEGORY, ranking: RANKING, sort: sort || null, sort_parts: SORT_PARTS, max_price: max, listed: [], new: [] };
|
|
299
316
|
for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
|
|
@@ -421,14 +438,24 @@ async function readListing(key, { db, floor = QUALITY_FLOOR, viewerId = null } =
|
|
|
421
438
|
if (held) placed = { placement: 'excluded', reason: 'over_cap' };
|
|
422
439
|
}
|
|
423
440
|
const overall = score && placed.placement !== 'new' ? score.overall : null;
|
|
441
|
+
// The price as the catalog shows it, and — to the author only — the ask to set a
|
|
442
|
+
// cash price when the module still carries only an old credit price (task
|
|
443
|
+
// 1004989; the owner's ruling, 2026-10-29). Read by idx_store_module_prices_current.
|
|
444
|
+
const { rows: [priceRow] } = await pool.query(
|
|
445
|
+
`SELECT price_cents, price_credits FROM store_module_prices
|
|
446
|
+
WHERE module_key = $1 ORDER BY set_at DESC, id DESC LIMIT 1`, [key]);
|
|
447
|
+
const price = priceOf(priceRow || null);
|
|
424
448
|
return {
|
|
425
449
|
...head, version: newest.version, version_id: newest.version_id, overall,
|
|
426
450
|
...explainPlacement(placed, { overall, floor, security, weaknesses: reading.weaknesses, held }),
|
|
427
451
|
security, ...reading,
|
|
452
|
+
price, buyable: buyableNow(price),
|
|
453
|
+
price_prompt: isAuthor && price.coming ? PRICE_COMING_PROMPT : null,
|
|
428
454
|
};
|
|
429
455
|
}
|
|
430
456
|
|
|
431
457
|
module.exports = {
|
|
432
458
|
QUALITY_FLOOR, STRONG_AT, SORT_PARTS, RANKING, CAP_PER_SUBCATEGORY, placeVersion, readParts,
|
|
433
459
|
sortByPart, priceOf, withinMaxPrice, rankListed, applyCap, listCatalog, explainPlacement, readListing,
|
|
460
|
+
PRICE_COMING_PROMPT,
|
|
434
461
|
};
|
|
@@ -198,6 +198,8 @@ const MODULE_GLOBS = {
|
|
|
198
198
|
'modules/market-watch/', 'tests/market_watch_module.mjs',
|
|
199
199
|
'tests/market_watch_data.mjs', 'tests/market_watch_data_db.mjs',
|
|
200
200
|
'tests/market_watch_run.mjs',
|
|
201
|
+
// task 1004941: the rules page (proposals, decisions, the run's prompt fence)
|
|
202
|
+
'tests/market_watch_rules.mjs', 'tests/market_watch_rules_db.mjs',
|
|
201
203
|
'tests/market_watch_ask.mjs', 'tests/market_watch_ask_db.mjs',
|
|
202
204
|
],
|
|
203
205
|
// --- builder prefs (wandering/skill/sound/render) + builder-needs ---
|
|
@@ -254,8 +254,11 @@ async function publishVersion(input, { db, beforeCommit } = {}) {
|
|
|
254
254
|
// A module with NO price row is free. The owner decided this (2026-09-29): nothing
|
|
255
255
|
// sets a price yet and who may sell is still open, so an unpriced module is
|
|
256
256
|
// acquirable at no cost, and an entitlement taken then is kept if a price is added
|
|
257
|
-
// later (ADR 0338 D2). A priced module is free when
|
|
258
|
-
//
|
|
257
|
+
// later (ADR 0338 D2). A priced module is free when its cash price is zero. An old
|
|
258
|
+
// row that offered it at 0 credits also stays free, as it was — its author offered
|
|
259
|
+
// it at nothing and no credit moves to have it (task 1004989; module-listing.js priceOf
|
|
260
|
+
// reads the same rule). An old row priced only in credits above 0 is NOT free:
|
|
261
|
+
// its cash price is still coming, and credits never buy a module.
|
|
259
262
|
function isFreePrice(priceRow) {
|
|
260
263
|
if (!priceRow) return true;
|
|
261
264
|
return priceRow.price_cents === 0 || priceRow.price_credits === 0;
|
|
@@ -387,20 +390,25 @@ async function latestVersions(keys, { db, channelsFor = null } = {}) {
|
|
|
387
390
|
// author may, and a delisted module takes no new price (it can no longer be
|
|
388
391
|
// acquired). The price is a HISTORY: each change appends a row and the newest is
|
|
389
392
|
// current, which is what the catalog (module-listing.js priceOf) and acquire
|
|
390
|
-
// (isFreePrice above) already read. `cents
|
|
391
|
-
//
|
|
392
|
-
// (core_257's CHECK), and 0 in either is free. A paid price may be set before
|
|
393
|
+
// (isFreePrice above) already read. `cents`: a whole number of 0 or more; 0 is
|
|
394
|
+
// free. A paid price may be set before
|
|
393
395
|
// buying exists (owner, 2026-10-26): the module is listed with it and marked not yet
|
|
394
396
|
// buyable, and acquire keeps refusing it without an entitlement. Price is never an
|
|
395
397
|
// input to the score (ADR 0343 D6). The history IS the audit of price changes:
|
|
396
398
|
// every row names who set it and when, and the row before it is what it changed
|
|
397
399
|
// from (the audit middleware also logs the request). One transaction, the module row
|
|
398
400
|
// locked FOR UPDATE as publishVersion does, so two changes at once each report the
|
|
399
|
-
// price they really replaced. Returns { ok: true, previous, price }
|
|
400
|
-
// {
|
|
401
|
-
|
|
401
|
+
// price they really replaced. Returns { ok: true, previous, price } or
|
|
402
|
+
// { ok: false, status, code, message }.
|
|
403
|
+
//
|
|
404
|
+
// CASH ONLY (task 1004989, ADR 0385 decision 2): credits never price a module, so
|
|
405
|
+
// a new row carries cents and nothing else (core_287 refuses price_credits in the
|
|
406
|
+
// database too). `previous` reports the replaced row's cash price, and `coming:
|
|
407
|
+
// true` when that row priced the module in credits only.
|
|
408
|
+
async function setPrice({ key, cents, builderId }, { db } = {}) {
|
|
402
409
|
const pool = db || require('./pool').pool;
|
|
403
410
|
const refuse = (status, code, message) => ({ ok: false, status, code, message });
|
|
411
|
+
if (!Number.isInteger(cents) || cents < 0) return refuse(400, 'bad_price', 'A price is a whole number of cents, 0 or more. 0 makes the module free.');
|
|
404
412
|
const client = await pool.connect();
|
|
405
413
|
try {
|
|
406
414
|
await client.query('BEGIN');
|
|
@@ -415,13 +423,13 @@ async function setPrice({ key, cents = null, credits = null, builderId }, { db }
|
|
|
415
423
|
`SELECT price_cents, price_credits FROM store_module_prices
|
|
416
424
|
WHERE module_key = $1 ORDER BY set_at DESC, id DESC LIMIT 1`, [key]);
|
|
417
425
|
await client.query(
|
|
418
|
-
'INSERT INTO store_module_prices (module_key, price_cents,
|
|
419
|
-
[key, cents,
|
|
426
|
+
'INSERT INTO store_module_prices (module_key, price_cents, set_by) VALUES ($1, $2, $3)',
|
|
427
|
+
[key, cents, builderId]);
|
|
420
428
|
await client.query('COMMIT');
|
|
421
429
|
return {
|
|
422
430
|
ok: true,
|
|
423
|
-
previous: prev ? { cents: prev.price_cents,
|
|
424
|
-
price: { cents
|
|
431
|
+
previous: prev ? { cents: prev.price_cents, coming: prev.price_cents == null && Number(prev.price_credits) > 0 } : null,
|
|
432
|
+
price: { cents },
|
|
425
433
|
};
|
|
426
434
|
} catch (e) {
|
|
427
435
|
try { await client.query('ROLLBACK'); } catch { /* the connection may be gone */ }
|
|
@@ -576,9 +576,9 @@ module.exports = function buildModulesRouter() {
|
|
|
576
576
|
}));
|
|
577
577
|
|
|
578
578
|
// POST /api/bongos/store/modules/:key/price — the module's author sets its price
|
|
579
|
-
// (task 1004894, criterion wa5-module-economy). Body: { cents
|
|
580
|
-
//
|
|
581
|
-
//
|
|
579
|
+
// (task 1004894, criterion wa5-module-economy). Body: { cents }, a whole number of
|
|
580
|
+
// 0 or more; 0 is free. A credit price is refused (task 1004989, ADR 0385
|
|
581
|
+
// decision 2: credits never price or buy a module). Appends a row to the price history; the
|
|
582
582
|
// newest is what the catalog shows and what acquire charges. A paid price may be
|
|
583
583
|
// set before buying exists (owner, 2026-10-26): the catalog marks it not yet
|
|
584
584
|
// buyable. Price never moves the score (ADR 0343 D6). The audit middleware records
|
|
@@ -588,21 +588,24 @@ module.exports = function buildModulesRouter() {
|
|
|
588
588
|
// author check inside setPrice: only the person who published the module prices it.
|
|
589
589
|
router.post('/store/modules/:key/price', auth.requireBuilder, auth.requirePermission('module.submit'),
|
|
590
590
|
asyncHandler('POST /store/modules/:key/price', async (req, res) => {
|
|
591
|
-
const
|
|
592
|
-
|
|
591
|
+
const body = req.body || {};
|
|
592
|
+
// Refused before anything else, by name, so an older CLI that still sends
|
|
593
|
+
// credits is told why rather than seeing a generic validation error.
|
|
594
|
+
if (body.credits != null) {
|
|
595
|
+
return res.fail('credit_price_refused', { status: 400, message: 'Credits cannot price a module: they measure contribution and are never spent. Set a cash price in cents (0 makes it free).' });
|
|
596
|
+
}
|
|
597
|
+
if (validateOrRespond(req, res, { cents: { type: 'integer', min: 0, max: 100000000 } })) return;
|
|
593
598
|
const { key } = req.params;
|
|
594
599
|
if (!KEY_RE.test(key)) {
|
|
595
600
|
return res.fail('bad_module_key', { status: 400, message: 'A module key is lowercase kebab-case.' });
|
|
596
601
|
}
|
|
597
|
-
const body = req.body || {};
|
|
598
602
|
const cents = body.cents ?? null;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return res.fail('bad_price', { status: 400, message: 'Give a price in cents, credits or both. 0 in either makes the module free.' });
|
|
603
|
+
if (cents === null) {
|
|
604
|
+
return res.fail('bad_price', { status: 400, message: 'Give a price in cents. 0 makes the module free.' });
|
|
602
605
|
}
|
|
603
|
-
const out = await setPrice({ key, cents,
|
|
606
|
+
const out = await setPrice({ key, cents, builderId: req.builder.id });
|
|
604
607
|
if (!out.ok) return res.fail(out.code, { status: out.status, message: out.message });
|
|
605
|
-
log.info({ key, builderId: req.builder.id, cents
|
|
608
|
+
log.info({ key, builderId: req.builder.id, cents }, `${key} price set`);
|
|
606
609
|
res.json({ ok: true, module_key: key, previous: out.previous, price: out.price });
|
|
607
610
|
}));
|
|
608
611
|
|
|
@@ -772,9 +775,14 @@ module.exports = function buildModulesRouter() {
|
|
|
772
775
|
let held = await checkEntitlement(holder);
|
|
773
776
|
if (!held.entitled) {
|
|
774
777
|
if (!r.free) {
|
|
778
|
+
// Never "pay with credits" (task 1004989): credits are never spent. A
|
|
779
|
+
// module still carrying only an old credit price has no cash price yet.
|
|
780
|
+
const coming = r.price && r.price.price_cents == null;
|
|
775
781
|
return res.fail('payment_required', {
|
|
776
782
|
status: 402,
|
|
777
|
-
message:
|
|
783
|
+
message: coming
|
|
784
|
+
? `"${key}" has no cash price yet: its author has been asked to set one. Credits cannot buy a module.`
|
|
785
|
+
: `"${key}" is priced, and buying a module is not available yet.`,
|
|
778
786
|
});
|
|
779
787
|
}
|
|
780
788
|
const g = await grantEntitlement({ ...holder, source: 'free', grantedBy: req.builder.id });
|
|
@@ -917,8 +925,7 @@ module.exports = function buildModulesRouter() {
|
|
|
917
925
|
// `?sort=<part>` orders the listed modules by the part a buyer cares about
|
|
918
926
|
// (task 1003800, sortByPart); the parts it accepts come back as sort_parts.
|
|
919
927
|
// Without it the listed modules are ranked quality first, price breaking ties
|
|
920
|
-
// (task 1003808, rankListed); `?max_cents=`
|
|
921
|
-
// that cost more.
|
|
928
|
+
// (task 1003808, rankListed); `?max_cents=` leaves out modules that cost more.
|
|
922
929
|
//
|
|
923
930
|
// rank: any-builder — a read of public store metadata, like /store/modules/latest:
|
|
924
931
|
// no entitlement taken, nothing changed.
|
|
@@ -930,16 +937,19 @@ module.exports = function buildModulesRouter() {
|
|
|
930
937
|
const names = SORT_PARTS.map((p) => `${p.title} (${p.part})`).join(', ');
|
|
931
938
|
return res.fail('bad_request', { status: 400, message: `The store can be sorted by one part of the score: ${names}.` });
|
|
932
939
|
}
|
|
933
|
-
// An optional maximum price (task 1003808): ?max_cents
|
|
934
|
-
//
|
|
940
|
+
// An optional maximum price (task 1003808): ?max_cents=, a whole number of at
|
|
941
|
+
// least 0. Free modules always pass. There is no credit cap (task 1004989):
|
|
942
|
+
// credits never price a module, so ?max_credits= is refused by name.
|
|
943
|
+
if (req.query && req.query.max_credits != null) {
|
|
944
|
+
return res.fail('bad_request', { status: 400, message: 'Credits do not price modules, so there is no maximum in credits. Use max_cents.' });
|
|
945
|
+
}
|
|
935
946
|
const maxPrice = {};
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (v == null || v === '') continue;
|
|
947
|
+
const v = req.query && req.query.max_cents;
|
|
948
|
+
if (v != null && v !== '') {
|
|
939
949
|
if (typeof v !== 'string' || !/^\d{1,9}$/.test(v)) {
|
|
940
|
-
return res.fail('bad_request', { status: 400, message:
|
|
950
|
+
return res.fail('bad_request', { status: 400, message: 'max_cents is a maximum price: a whole number, 0 or more.' });
|
|
941
951
|
}
|
|
942
|
-
maxPrice
|
|
952
|
+
maxPrice.cents = Number(v);
|
|
943
953
|
}
|
|
944
954
|
const listing = await listCatalog({ sort, maxPrice });
|
|
945
955
|
res.set('Cache-Control', 'no-store');
|
|
@@ -1329,6 +1329,7 @@ function mountInternalSurfaces(app) {
|
|
|
1329
1329
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1330
1330
|
// page that embeds them.
|
|
1331
1331
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
|
1332
|
+
const MARKET_WATCH_PAGE_RE = /^\/builders\/market-watch(?:-[a-z]+)?(?:\.html)?\/?$/; // task 1004941: every read behind them is requireBuilder
|
|
1332
1333
|
app.use((req, res, next) => {
|
|
1333
1334
|
if (isModuleSurfaceHost(req)) return next();
|
|
1334
1335
|
// Cheap early-out: only /builders/* paths can ever be gated, so skip the
|
|
@@ -1342,7 +1343,7 @@ function mountInternalSurfaces(app) {
|
|
|
1342
1343
|
// behind the proxy's path normalization.
|
|
1343
1344
|
const norm = path.posix.normalize(req.path);
|
|
1344
1345
|
if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || SIGNEDIN_BUILDERS_PREFIX_RE.test(norm)
|
|
1345
|
-
|| TASK_DETAIL_PAGE_RE.test(norm) || IDEA_DETAIL_PAGE_RE.test(norm)) {
|
|
1346
|
+
|| TASK_DETAIL_PAGE_RE.test(norm) || IDEA_DETAIL_PAGE_RE.test(norm) || MARKET_WATCH_PAGE_RE.test(norm)) {
|
|
1346
1347
|
return gdsAuth.requireBuilderPage(req, res, next);
|
|
1347
1348
|
}
|
|
1348
1349
|
next();
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.9'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -222,6 +222,7 @@ for (const rel of [
|
|
|
222
222
|
'modules/security/routes/reports.js',
|
|
223
223
|
'modules/onboarding/routes/access-requests.js',
|
|
224
224
|
'modules/government/routes/government.js',
|
|
225
|
+
'modules/market-watch/routes/rules.js',
|
|
225
226
|
]) app.use(require(path.join(ROOT, rel))());
|
|
226
227
|
const server = http.createServer(app);
|
|
227
228
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
@@ -243,6 +244,8 @@ const REQUESTS = {
|
|
|
243
244
|
'PATCH /project-settings/ship-mode': ['PATCH', '/project-settings/ship-mode', { mode: 'sometimes' }],
|
|
244
245
|
'PATCH /project-settings/tweak-autopickup': ['PATCH', '/project-settings/tweak-autopickup', { mode: 'sometimes' }],
|
|
245
246
|
'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
|
|
247
|
+
// task 1004941: the owner deciding a Market Watch proposal
|
|
248
|
+
'POST /market-watch/proposals/:id/decide': ['POST', `/market-watch/proposals/${BAD}/decide`, { decision: 'approve' }],
|
|
246
249
|
'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
|
|
247
250
|
'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
|
|
248
251
|
'POST /security/reports/:id/fix': ['POST', `/security/reports/${BAD}/fix`, {}],
|
|
@@ -475,7 +475,10 @@ test('a matter\'s act is checked when it is opened: People only, a custom rank o
|
|
|
475
475
|
openBoardItem: async () => { throw new Error('must not be reached'); },
|
|
476
476
|
}, async () => {
|
|
477
477
|
const act = (a) => ({ type: 'assign_rank', builder_id: 7, rank_key: 'tester', ...a });
|
|
478
|
-
|
|
478
|
+
// task 1004941: Rules now carries one act of its own (a Market Watch proposal), so a
|
|
479
|
+
// People act there is the wrong act; a building with no acts still refuses any act
|
|
480
|
+
assert.equal((await board.openMatter({ area: 'rules', title: 't', action: act(), proposedBy: '42' })).reason, 'bad_action');
|
|
481
|
+
assert.equal((await board.openMatter({ area: 'planning', title: 't', action: act(), proposedBy: '42' })).reason, 'action_not_supported_here');
|
|
479
482
|
assert.equal((await board.openMatter({ area: 'people', title: 't', action: act({ rank_key: 'metic' }), proposedBy: '42' })).reason, 'seeded_rank');
|
|
480
483
|
assert.equal((await board.openMatter({ area: 'people', title: 't', action: act({ rank_key: 'ghost' }), proposedBy: '42' })).reason, 'unknown_rank');
|
|
481
484
|
assert.equal((await board.openMatter({ area: 'people', title: 't', action: act({ type: 'promote' }), proposedBy: '42' })).reason, 'bad_action');
|