@bongos/core 1.19.914 → 1.19.916
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 +58 -18
- package/docs/adr/0316-the-ignite-floor-is-not-the-trusted-tier.md +98 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/board.js +1 -1
- package/modules/ideas/config.js +230 -0
- package/modules/ideas/grades.js +317 -0
- package/modules/ideas/inbox.js +64 -720
- package/modules/ideas/landing-preview.js +9 -1
- package/modules/ideas/projection.js +103 -0
- package/modules/ideas/ratify.js +211 -0
- package/modules/ideas/reads.js +96 -0
- package/modules/ideas/routing.js +19 -2
- package/modules/ideas/trail.js +111 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/idea_cascade.mjs +3 -3
- package/tests/ideas_config.mjs +271 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.916",
|
|
6
|
+
"core_contract": "1.19.916",
|
|
7
|
+
"source_commit": "47aafa7b40067f457043c59439b6f75ce58010b7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-20T17:23:41.647Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 505,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2294,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2825,
|
|
20
|
+
"tree_sha256": "9fb2e229c3f92268ce44fe9fe75818d005889103f22ac2bf06d12ecfe2e86d84",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2069,10 +2069,15 @@
|
|
|
2069
2069
|
"mode": "0000644",
|
|
2070
2070
|
"sha256": "bc9dde45a33790e4fbce47682a5734a1adaf4fdd7f9347bb0128c6cca6fcf4e5"
|
|
2071
2071
|
},
|
|
2072
|
+
{
|
|
2073
|
+
"path": "docs/adr/0316-the-ignite-floor-is-not-the-trusted-tier.md",
|
|
2074
|
+
"mode": "0000644",
|
|
2075
|
+
"sha256": "d0597a50d8f9a23c8d6c81a595fe6af73640cf574ae32f64730078ac7e19c7a5"
|
|
2076
|
+
},
|
|
2072
2077
|
{
|
|
2073
2078
|
"path": "docs/adr/README.md",
|
|
2074
2079
|
"mode": "0000644",
|
|
2075
|
-
"sha256": "
|
|
2080
|
+
"sha256": "005b960dede2131ea0e2f3b81afcb119d7b65ddb6ed81bb246896eff64a69320"
|
|
2076
2081
|
},
|
|
2077
2082
|
{
|
|
2078
2083
|
"path": "docs/api-reference.md",
|
|
@@ -2642,7 +2647,7 @@
|
|
|
2642
2647
|
{
|
|
2643
2648
|
"path": "docs/module-api-changelog.md",
|
|
2644
2649
|
"mode": "0000644",
|
|
2645
|
-
"sha256": "
|
|
2650
|
+
"sha256": "ef9352b603ffafd1863959527fd70807e22baeee2daf3234155f0215e8b7c42c"
|
|
2646
2651
|
},
|
|
2647
2652
|
{
|
|
2648
2653
|
"path": "docs/modules-contract.md",
|
|
@@ -4272,7 +4277,7 @@
|
|
|
4272
4277
|
{
|
|
4273
4278
|
"path": "modules/government/board.js",
|
|
4274
4279
|
"mode": "0000644",
|
|
4275
|
-
"sha256": "
|
|
4280
|
+
"sha256": "88a52649894a03171ae46d6e517b9828923e033eaf161f387c7434cd78871750"
|
|
4276
4281
|
},
|
|
4277
4282
|
{
|
|
4278
4283
|
"path": "modules/government/catalog.js",
|
|
@@ -5334,11 +5339,21 @@
|
|
|
5334
5339
|
"mode": "0000644",
|
|
5335
5340
|
"sha256": "07e5666362c91f92d34388b4f6f4507cc5387866d317970478e6cae417c19975"
|
|
5336
5341
|
},
|
|
5342
|
+
{
|
|
5343
|
+
"path": "modules/ideas/config.js",
|
|
5344
|
+
"mode": "0000644",
|
|
5345
|
+
"sha256": "70caf7fe901380679ec4a418056e2688efe4afe26d6d5a6058ff64fa36445109"
|
|
5346
|
+
},
|
|
5337
5347
|
{
|
|
5338
5348
|
"path": "modules/ideas/descent.js",
|
|
5339
5349
|
"mode": "0000644",
|
|
5340
5350
|
"sha256": "778558f148d8b403b64f0714b27436b08c41ae0d21ed18b4ed73939928e05cd4"
|
|
5341
5351
|
},
|
|
5352
|
+
{
|
|
5353
|
+
"path": "modules/ideas/grades.js",
|
|
5354
|
+
"mode": "0000644",
|
|
5355
|
+
"sha256": "a445024a9da1f91d1b5842066599e12422f6252a71fd0c30c4a60c2ecfb6ae51"
|
|
5356
|
+
},
|
|
5342
5357
|
{
|
|
5343
5358
|
"path": "modules/ideas/homeless-home.js",
|
|
5344
5359
|
"mode": "0000644",
|
|
@@ -5347,12 +5362,12 @@
|
|
|
5347
5362
|
{
|
|
5348
5363
|
"path": "modules/ideas/inbox.js",
|
|
5349
5364
|
"mode": "0000644",
|
|
5350
|
-
"sha256": "
|
|
5365
|
+
"sha256": "85d629f2ac8ad36dd7dfd274a95ac40d9a7dfe3501dbff207dedc7846d981e53"
|
|
5351
5366
|
},
|
|
5352
5367
|
{
|
|
5353
5368
|
"path": "modules/ideas/landing-preview.js",
|
|
5354
5369
|
"mode": "0000644",
|
|
5355
|
-
"sha256": "
|
|
5370
|
+
"sha256": "735571069c131d0ee9f88b0925b5dc50c0583f295220027ab88e791bc940fe3c"
|
|
5356
5371
|
},
|
|
5357
5372
|
{
|
|
5358
5373
|
"path": "modules/ideas/migrations/ideas_001_grades.sql",
|
|
@@ -5399,6 +5414,21 @@
|
|
|
5399
5414
|
"mode": "0000644",
|
|
5400
5415
|
"sha256": "80faf8d20df4f051000460a0453472bd1a177ca4e9e9a31e8c5b7f3065575995"
|
|
5401
5416
|
},
|
|
5417
|
+
{
|
|
5418
|
+
"path": "modules/ideas/projection.js",
|
|
5419
|
+
"mode": "0000644",
|
|
5420
|
+
"sha256": "2548cb8ac60664efaf11221333d445bb3ce07365b7e98d64f041cef60fd2fd53"
|
|
5421
|
+
},
|
|
5422
|
+
{
|
|
5423
|
+
"path": "modules/ideas/ratify.js",
|
|
5424
|
+
"mode": "0000644",
|
|
5425
|
+
"sha256": "2988a93940cd1eba73fb0c9e04f8326108466ba81901f86b3768b29115f17237"
|
|
5426
|
+
},
|
|
5427
|
+
{
|
|
5428
|
+
"path": "modules/ideas/reads.js",
|
|
5429
|
+
"mode": "0000644",
|
|
5430
|
+
"sha256": "d7a55415e6fdfca47bca5579ceac0e877de0b1a041b5173253f43d3a9a90d1b4"
|
|
5431
|
+
},
|
|
5402
5432
|
{
|
|
5403
5433
|
"path": "modules/ideas/routes/blockers.js",
|
|
5404
5434
|
"mode": "0000644",
|
|
@@ -5412,7 +5442,7 @@
|
|
|
5412
5442
|
{
|
|
5413
5443
|
"path": "modules/ideas/routing.js",
|
|
5414
5444
|
"mode": "0000644",
|
|
5415
|
-
"sha256": "
|
|
5445
|
+
"sha256": "ec26fc6a2481e1f13d1141f6f1248132ed282f73ddba37c525af5ecae7fd1f0e"
|
|
5416
5446
|
},
|
|
5417
5447
|
{
|
|
5418
5448
|
"path": "modules/ideas/skill-friction.js",
|
|
@@ -5434,6 +5464,11 @@
|
|
|
5434
5464
|
"mode": "0000644",
|
|
5435
5465
|
"sha256": "f1e60dbc8c6dc0e9f5f5cb93bf26c659422503e8ec66937a3dcee5b64971e995"
|
|
5436
5466
|
},
|
|
5467
|
+
{
|
|
5468
|
+
"path": "modules/ideas/trail.js",
|
|
5469
|
+
"mode": "0000644",
|
|
5470
|
+
"sha256": "35dfa6abb78efdac695b89805d660ff4bb0e0d2bc2587f0d1945310dcffb2980"
|
|
5471
|
+
},
|
|
5437
5472
|
{
|
|
5438
5473
|
"path": "modules/lifecycle/CLAUDE.md",
|
|
5439
5474
|
"mode": "0000644",
|
|
@@ -7797,12 +7832,12 @@
|
|
|
7797
7832
|
{
|
|
7798
7833
|
"path": "package-lock.json",
|
|
7799
7834
|
"mode": "0000644",
|
|
7800
|
-
"sha256": "
|
|
7835
|
+
"sha256": "6da05875d670e2a4abe086ff6b24c3dd762dbb82943a155050be68774fde0516"
|
|
7801
7836
|
},
|
|
7802
7837
|
{
|
|
7803
7838
|
"path": "package.json",
|
|
7804
7839
|
"mode": "0000644",
|
|
7805
|
-
"sha256": "
|
|
7840
|
+
"sha256": "68a643c532ef428f18f6232a001fad93cceeb3f8af6ef1f4f5b853e21cd52163"
|
|
7806
7841
|
},
|
|
7807
7842
|
{
|
|
7808
7843
|
"path": "public-docs/index.html",
|
|
@@ -9802,7 +9837,7 @@
|
|
|
9802
9837
|
{
|
|
9803
9838
|
"path": "src/module-api.js",
|
|
9804
9839
|
"mode": "0000644",
|
|
9805
|
-
"sha256": "
|
|
9840
|
+
"sha256": "df4b0f103f4790210ad2f94b8431893d4ff4e43faa88004ddb952b25cf1735b8"
|
|
9806
9841
|
},
|
|
9807
9842
|
{
|
|
9808
9843
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11812,7 +11847,7 @@
|
|
|
11812
11847
|
{
|
|
11813
11848
|
"path": "tests/idea_cascade.mjs",
|
|
11814
11849
|
"mode": "0000644",
|
|
11815
|
-
"sha256": "
|
|
11850
|
+
"sha256": "7e49b96f4c3365b0d7240d92775022fdea988049d30041885e13eca61f18e5b1"
|
|
11816
11851
|
},
|
|
11817
11852
|
{
|
|
11818
11853
|
"path": "tests/idea_completeness.mjs",
|
|
@@ -11959,6 +11994,11 @@
|
|
|
11959
11994
|
"mode": "0000644",
|
|
11960
11995
|
"sha256": "c7ba20839a7b33df0eb7e8bfc2f463217e7d3a538d888e89774107d55d34556d"
|
|
11961
11996
|
},
|
|
11997
|
+
{
|
|
11998
|
+
"path": "tests/ideas_config.mjs",
|
|
11999
|
+
"mode": "0000644",
|
|
12000
|
+
"sha256": "35334314ea15cfa5cd262c18def917bea04f569cfa52a0a86b663710de2bd172"
|
|
12001
|
+
},
|
|
11962
12002
|
{
|
|
11963
12003
|
"path": "tests/idp_device_exchange.mjs",
|
|
11964
12004
|
"mode": "0000644",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ADR 0316 — The ignite floor is a scheduling dial, not the trusted tier
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-20
|
|
5
|
+
- **Task:** 1004084 (BV2.R05) · goal 1000095 · spec `docs/specs/<redacted>.md` decisions 3 and 16
|
|
6
|
+
- **Supersedes nothing.** Narrows how spec decision 3 is to be read.
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
BV2 decision 3 makes the **ignite floor** — who may turn a quick idea directly into
|
|
11
|
+
claimable work — a per-instance setting: "a project of one wants every thought
|
|
12
|
+
claimable; a project of fifty wants a nod." Decision 16 adds a second dial, the
|
|
13
|
+
**descent decay rate**, and rules that the two should share one home rather than
|
|
14
|
+
growing separately.
|
|
15
|
+
|
|
16
|
+
Both the spec and the seeded task described the implementation the same way: the
|
|
17
|
+
ignite floor *is* the `METIC_PLUS` constant in `modules/ideas/routing.js`, so this
|
|
18
|
+
is "a setting, not a refactor."
|
|
19
|
+
|
|
20
|
+
Tracing the code before building showed that premise is false. `METIC_PLUS` is
|
|
21
|
+
spelled once but **read for four semantically different gates**:
|
|
22
|
+
|
|
23
|
+
1. `decideLanding`, the homeless-bug branch — land at `ready` or at `backlog`.
|
|
24
|
+
2. `decideLanding`, the goal-carrying bug branch — the same split.
|
|
25
|
+
3. `mayProposeCriterion` (and the 403 in `routes/inbox.js`) — who may file a
|
|
26
|
+
`kind='criterion-proposal'`, a planning-scope write that reshapes a version's
|
|
27
|
+
scope-truth.
|
|
28
|
+
4. The protected-scope wall in `authorizeRoutedLanding` — who may route work into
|
|
29
|
+
the permission / ship-grade / deploy / migration core.
|
|
30
|
+
|
|
31
|
+
Only (1) and (2) are the ignite floor. Making the *constant* configurable, as
|
|
32
|
+
written, would mean an instance setting its floor to `xenos` — precisely the
|
|
33
|
+
"project of one" case the owner asked for — would **also** grant every Xenos the
|
|
34
|
+
right to file criterion-proposals and to put rows into the protected core's queue.
|
|
35
|
+
|
|
36
|
+
The codebase already said so, in three places, and none of them had been read into
|
|
37
|
+
the spec:
|
|
38
|
+
|
|
39
|
+
- `routing.js` at the constant's definition: "It grants nothing — the server-side
|
|
40
|
+
rank floor and the goal's scope wall are separate, load-bearing gates."
|
|
41
|
+
- The protected-scope branch: "the two are layers, not alternatives."
|
|
42
|
+
- `tests/filing_abuse_matrix.mjs` models the widening as an **attack**, recording
|
|
43
|
+
in its header that "METIC_PLUS widened to include xenos ... 4 of 23 fail", and
|
|
44
|
+
pinning the literal.
|
|
45
|
+
|
|
46
|
+
## Decision
|
|
47
|
+
|
|
48
|
+
**The ignite floor is a separate dial from the trusted tier, and only the dial is
|
|
49
|
+
configurable.**
|
|
50
|
+
|
|
51
|
+
- A new per-instance config home, `modules/ideas/config.js` +
|
|
52
|
+
`config/ideas.neutral.json`, carries both dials. Precedence mirrors
|
|
53
|
+
`modules/government/config.js`: env override → `config/ideas.json` at the
|
|
54
|
+
**instance** root → `config/ideas.neutral.json` at the **core** root. The
|
|
55
|
+
dual-root split matters here for the reason ADR 0268 records.
|
|
56
|
+
- `igniteFloor` defaults to `metic`, which resolves to exactly the `METIC_PLUS`
|
|
57
|
+
set, so the default is a true no-op. It is threaded into `decideLanding` as a
|
|
58
|
+
ready-made `trustedRanks` Set by the callers, keeping `routing.js` pure and
|
|
59
|
+
free of both config and DB.
|
|
60
|
+
- `descentDecayRate` defaults to `0.5`. R11 is its intended consumer and has not
|
|
61
|
+
landed, so it ships as a contract with no production caller — the same
|
|
62
|
+
deliberate slice boundary `modules/government/config.js` documents for itself,
|
|
63
|
+
and what decision 16 asked for when it said the two dials should share one home.
|
|
64
|
+
- **`METIC_PLUS` stays a fixed constant** for gates (3) and (4). Neither takes a
|
|
65
|
+
floor argument, and `tests/ideas_config.mjs` asserts structurally that
|
|
66
|
+
`authorizeRoutedLanding`'s signature never grows one.
|
|
67
|
+
|
|
68
|
+
## Consequences
|
|
69
|
+
|
|
70
|
+
- An instance may open its ignite floor all the way to `xenos` without opening any
|
|
71
|
+
authority surface. The worst a wide-open floor can do is put a row at `ready`
|
|
72
|
+
that a human can move back to `backlog`.
|
|
73
|
+
- The `filing_abuse_matrix` pin on the `METIC_PLUS` literal stays green and is not
|
|
74
|
+
to be relaxed. A future change that needs to relax it is almost certainly the
|
|
75
|
+
widening this ADR exists to refuse.
|
|
76
|
+
- `modules/ideas/config.js` derives the rank ladder from `api.LIVE_RANK_LADDER`
|
|
77
|
+
on the doorway. The first cut restated it as a literal, reasoning that the file
|
|
78
|
+
must resolve at load with no port wired; `tests/rank_tier_single_source.mjs`
|
|
79
|
+
rejected that — a ladder array literal in a tracked `src`/`modules`/`scripts`
|
|
80
|
+
file fails CI unless allowlisted — and it was right to. The doorway value is a
|
|
81
|
+
lazy getter over the kernel this module's callers already load, so deriving
|
|
82
|
+
costs nothing and removes the drift risk instead of merely detecting it.
|
|
83
|
+
- Spec decision 3's phrase "spelled in one place, so this is a setting, not a
|
|
84
|
+
refactor" should be read as describing the *intent*, not the mechanism. The
|
|
85
|
+
spec was not edited; this ADR is the correction of record, and the task
|
|
86
|
+
description was amended in-flight to match.
|
|
87
|
+
|
|
88
|
+
## Alternatives considered
|
|
89
|
+
|
|
90
|
+
- **Make `METIC_PLUS` itself configurable, as written.** Rejected: it is a
|
|
91
|
+
security widening wearing a config file's clothes, and the repo's own tests
|
|
92
|
+
already classify it as an attack.
|
|
93
|
+
- **Split `METIC_PLUS` into four named constants and configure one.** Rejected as
|
|
94
|
+
more churn than the decision needs — the other three gates want the *same* fixed
|
|
95
|
+
tier, and giving them separate names invites someone to configure them later.
|
|
96
|
+
- **Have `routing.js` read the config itself.** Rejected: it would make the
|
|
97
|
+
landing matrix non-pure and file-system-dependent, and would remove the
|
|
98
|
+
callers' ability to be tested at a different floor.
|
package/docs/adr/README.md
CHANGED
|
@@ -406,4 +406,5 @@ This keeps the decision history honest and traceable.
|
|
|
406
406
|
| 0312 | [**Provenance is a SECOND ideas-owned edge table, written by the seams that already know** ([task 1004080](https://cloudbongos.com/builders#/task/1004080), goal 1000095 — BONGOS-V2). The spike [ADR 0311](0311-an-idea-is-a-permanent-origin-tag.md) deferred: how the descent edges are physically recorded. A descent (`idea → task → rewrite → split → removal`) is four levels deep and crosses a version boundary, and the task hierarchy refuses both — `attachParent` enforces `SELF_PARENT`, `VERSION_MISMATCH`, `PARENT_HAS_PARENT` and `TARGET_HAS_CHILDREN`, all load-bearing for ship rollup and claim safety. The spike was told to CONFIRM `ideas_task_lineage` rather than assume it, and confirmed one half while refuting the other: the table is right as the **idea→task anchor** (bipartite, no depth cap, no version predicate) but CANNOT carry the task→task edge, because a derivation is a property of the task PAIR and [ADR 0236](0236-many-ideas-one-task-the-lineage-key-is-the-pair.md) made co-source ideas both legal and common — a `derived_from_task_id` column would write the same derivation once per co-source idea and let two rows disagree about where a task came from, the same under-keying that silently lost attribution on prod once already. A merge also gives a task SEVERAL parents, so the relation is a **DAG, not a tree**. Decision: a new `ideas_task_descent` (`task_id`, `parent_task_id`, CHECK'd `relation`, PK on the pair, indexed both ways), with NO depth cap and NO version predicate, and **cycles refused at write time** because the payout walks this graph and a cycle is a non-terminating walk rather than a cosmetic error. On who writes it — the question the task actually set, since an edge depending on memory will have holes — **three seams already know and none is telling anyone**: `cascade.js` encodes the originating task id inside `source_ref` as a STRING nothing can join on, `tasks.merged_into_task_id` already records the fold, and `parent_task_id` already records the split. Only `rewrite` and cross-version `merge` stay manual, which is honest — they are editorial acts with no automatic signal, and their absence degrades the picture rather than corrupting it. What it fixes: `sourceIdeaForTask` and `awardIdeaTaskShipCredit` are both one-hop `LIMIT 1` point reads, so a DERIVED task resolves to `no_source_idea` and the origin is not paid at all. Does not decide: the decay rate beyond its default, what counts as removed ([ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md)), or how any of it is drawn.](0312-provenance-is-a-second-ideas-owned-edge-table.md) | ideas / lifecycle |
|
|
407
407
|
| 0313 | [**A removal is derived from the SERVER's own merge diff and confirmed by a person** ([task 1004081](https://cloudbongos.com/builders#/task/1004081), goal 1000095 — BONGOS-V2). The other half [ADR 0311](0311-an-idea-is-a-permanent-origin-tag.md) deferred: what counts as removed, so that brightness can fall at all. Nothing records it today — `shipped` is terminal (`rot.js` says so outright) and `merged_into_task_id` is CHECK'd to `abandoned` rows, describing only work cut BEFORE it landed. Owner decision 13 set the shape (system proposes, person confirms); this settles the detail, and corrects two premises the task was written on. **`cascade.js` is no longer empty** — it carries a production rule and [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) fixed the contract (append an entry, never touch the runner), so there is a precedent to copy including its anti-recursion guard. **`netNegativeBonusAmount` cannot be the trigger** despite being the named nearest signal: it is an aggregate with NO TARGET (it says deletion happened, never what or whose), it is CLIENT-SUPPLIED via `signals.diff_stats`, and `grader-score.js` already documents the forgery it defends against — and brightness sits beside a payout, so a forgeable target-less counter cannot carry it. **Nothing durable records what any ship touched**: `committed_files` and `diff_stats` are client-supplied at grade time and never persisted, `touches[]` is advisory ([ADR 0049](0049-split-parallel-safety-contract.md)), so a removal cannot be reconstructed from history — only derived as a ship lands. Decision: derive it from the SERVER's own merge diff ([ADR 0055](0055-server-mediated-branch-publish.md) — the server holds the repo and does the merge, so it computes the diff rather than being told one), and attribute the deleted hunks through **`claims.published_head_sha`** (`migrations/171`), NOT by parsing `(task NNNN)` out of commit text — only 7 of `main`'s last 15 commits carry a task id, and a convention that holds most of the time cannot underwrite a payout. An UNATTRIBUTABLE deletion files nothing: a wrong proposal costs attention and dims a real star. A rename, a move and a rewrite-in-place are NOT removals, and a cold body is not a remnant — removal applies only below a task that actually shipped. The proposal is an appended `CASCADES` entry filing a QUESTION; confirmation is Metic+/`task.promote` on the existing awaiting-nod surface; an unanswered one rots on the normal clock. Only a CONFIRMED proposal dims a star. The derivation must be NON-BLOCKING — landing work matters more than dimming a star on time. Brightness can only fall forward from here, so historical sizes need a marker or the number lies.](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md) | ideas / lifecycle |
|
|
408
408
|
| 0314 | [**A module owns a protected path without the whole module becoming protected** ([task 1004070](https://cloudbongos.com/builders#/task/1004070), goal 1000095 — BONGOS-V2). Three modules derive `protected` — and so are Archon-only to scope a goal to — because of ONE file that is not their domain: `onboarding` on `scripts/gds/setup.js`, `autonomy` on `scripts/gds/conductor-dispatch.js`, `copy-desk` on `scripts/gds/copy-inventory.js`. The owner chose *narrow what counts as protected*; the task set the fork and required the choice be justified before any glob moved. **Deleting the glob is out**: `MODULE_GLOBS` is also the territory deciding who may AUTHOR a task on a path, and the shared floor is only `tests/`, so an unowned path is authorable by no goal at all. That left reassigning each CLI to an already-protected module — **measured, not reasoned about**, by applying the edit to the source and re-requiring in a fresh process (`MODULE_SCOPE_MAP` is frozen and built from a COPY of `MODULE_GLOBS` at load, so an in-process mutation proves nothing and passes green). The measurement kills it: `copy-desk` and `autonomy` lose the ability to author on their own generator and their own conductor, and — the row that decides it — `devsecops` as the new owner **cannot author the generator together with the two artifacts it writes**, because `docs/copy-registry.json` and `docs/copy-inventory.md` stay `copy-desk`'s. So *change a UI string, regenerate the registry* becomes authorable by no single goal, and that is the ship-check freshness gate every UI task hits. Decision: **protection becomes per-path at the goal-scope wall, as it already is at the other three** — `matchProtected` decides task-authoring, the grader and gate decide ship, the post-push audit decides the dishonest case; only goal-creation asked about the MODULE, and that inconsistency is the whole defect. The derivation stays computed from `PROTECTED_GLOBS` and no flag is hand-set ([ADR 0086](0086-goal-scoped-work-hierarchy.md) §7 / SEC #863), and the Metic floor on those paths does not move. **Corrects the task's own numbers**: 12 of 26 modules derive protected, not 11, and `agents` (protected by `modules/agents/`, itself a protected glob) is absent from its list of seven — which is why the one thing this ADR SHIPS is a test pinning the full protected key set **by name**, a count assertion passing on the wrong twelve being exactly how that drifted. Implementation is deferred to task 1004109, per the task's own instruction to stop after the ADR. Also filed: `builder-settings` (task 1004110), whose overlap rides the blanket `src/bongos/routes/` prefix and so needs either the `me.js` strangler move or its own security ADR; and **nine protected paths owned by no module at all** (task 1004111) — the freshness generators, the merge-driver pair and `cli-lib.js` among them — found while checking whether deletion was viable, a pre-existing hole rather than one this opens. Does not decide: the floor a protected path carries, whether `scripts/gds/` deserves a shared-floor entry the way `tests/` has one, or how the nine unowned paths get a home.](0314-module-protection-is-per-path-not-per-module.md) | modules / permissions |
|
|
409
|
+
| 0316 | [**The ignite floor is a scheduling dial, not the trusted tier** ([task 1004084](https://cloudbongos.com/builders#/task/1004084), goal 1000095 — BONGOS-V2). BV2 decision 3 makes the ignite floor — who may turn a quick idea straight into claimable work — a per-instance setting, and decision 16 adds the descent decay rate beside it, the two to share one home. Both the spec and the seeded task described the mechanism the same way: the floor IS the `METIC_PLUS` constant in `modules/ideas/routing.js`, so this is "a setting, not a refactor". Tracing the code before building refuted that: `METIC_PLUS` is spelled once but READ for four semantically different gates — the two `decideLanding` bug branches (land at `ready` or at `backlog`), `mayProposeCriterion` (who may file a `kind='criterion-proposal'`, a planning-scope write), and the protected-scope wall in `authorizeRoutedLanding` (who may route work into the permission / ship-grade / deploy / migration core). Only the first two are the ignite floor, so making the CONSTANT configurable would mean an instance setting its floor to `xenos` — exactly the "project of one wants every thought claimable" case the owner asked for — ALSO gaining the right to file criterion-proposals and to breach the protected core. The codebase already said so in three places nobody had read into the spec: the constant's own header ("It grants nothing — the server-side rank floor and the goal's scope wall are separate, load-bearing gates"), the protected-scope branch ("the two are layers, not alternatives"), and `tests/filing_abuse_matrix.mjs`, which models the widening as an ATTACK and pins the literal. Decision: a separate dial. `modules/ideas/config.js` + `config/ideas.neutral.json` carry both knobs on the `modules/government/config.js` precedence (env -> instance root `config/ideas.json` -> core root neutral; the ADR 0268 dual-root split matters, since one root reads the host pack out of `node_modules` where `npm ci` erases it). `igniteFloor` defaults to `metic`, resolving to exactly the `METIC_PLUS` set, and is threaded into `decideLanding` as a ready-made `trustedRanks` **Set** by the callers — `routing.js` is TOLD the floor and never reads config, staying pure and DB-free — while `METIC_PLUS` stays a FIXED constant for the other two gates, neither of which takes a floor argument (pinned structurally). `descentDecayRate` defaults to 0.5 and ships with no production caller, because R11 is its intended reader and depends on the unbuilt R08 — the same deliberate slice boundary `modules/government/config.js` documents for itself, and what decision 16 asked for. Everything fails to VANILLA rather than dead: a malformed pack, an unknown rank or an out-of-band rate falls back, and an unrecognised floor yields the DEFAULT set, never an empty one (failing closed would stop every ignition on a typo). Rejected: configuring `METIC_PLUS` as written (a security widening in a config file's clothes); splitting it into four named constants (the other three want the same fixed tier, and naming them invites configuring them); having `routing.js` read the config itself (makes the landing matrix impure and untestable at a different floor). Spec decision 3's "spelled in one place" describes the intent, not the mechanism; the spec was not edited and the task description was amended in flight.](0316-the-ignite-floor-is-not-the-trusted-tier.md) | ideas / permissions |
|
|
409
410
|
| 0315 | [**A plane with no proxy is refused, not given a no-op ingress** ([task 1004017](https://cloudbongos.com/builders#/task/1004017), goal 1000106 — BONGOS-V2). The piece [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) explicitly deferred: the self-host image ships no proxy, so provisioning step 6's `<slug>.caddy` snippet write and `systemctl reload caddy` cannot work there and a child instance gets no front door. **Two failure shapes, only one visible**: a DOMAINED child already half-refused, because `caddy adapt` fails with no binary and [ADR 0303](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)'s own catch turns that into a gap — but the message told the operator to add a directive to `/etc/caddy/Caddyfile`, a file the container does not have and that ADR 0303 §1 forbids provisioning from creating; a DOMAINLESS child skipped that preflight entirely (it is gated on `inst.domain`), reached step 6 and died on the `sudo` write, an uncaught throw that bypasses the `{ok:false}` pending-retry path and hard-errors the instance on attempt one — a crash recorded where a refusal was the honest answer. Decision: `ingressPlaneGap(exec)` probes the plane with `caddy version` — one cheap unprivileged read, the same posture as `checkCaddyWiring` — and both standup legs refuse with `Ingress preflight failed: …`, deliberately NOT domain-gated, because the domainless child is the one 0303 could not see. **Only a missing binary proves there is no proxy**: under remote exec `boxExec` is `ssh <host> bash -s`, so an unreachable co-hosting box throws here too, and diagnosing a network blip as 'this plane has no proxy, provision elsewhere' would be a confident wrong answer that burns the intent's retry budget — exit 127 is read as proof, anything else answers 'could not confirm a reverse proxy on this plane' with the real cause leading. The three refusals are ordered **capacity → ingress → wiring**, cheapest and most fundamental first and all before any mutation, which moves 0303's preflight behind the ADR 0285 capacity measurement; both still refuse before anything changes, so only the precedence between two refusals moves. **No ingress seam and no no-op backend**, despite the obvious symmetry with 0306: a `none` implementation whose `writeSite`/`reload` are no-ops lets the standup SUCCEED while leaving the child unreachable — neither half of the task's done-when, and exactly the silently-broken instance it exists to prevent. Does not decide: what a child's front door IS on a container plane (a published port, a bundled proxy, an explicit localhost-only shape) — a future ADR that adds one replaces this refusal rather than extending it. Teardown is left alone on purpose: it already reloads `allowFail`, and refusing there would strand a row instead of cleaning it.](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md) | provisioning / ingress |
|
|
@@ -2302,5 +2302,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2302
2302
|
landed since 1.19.912 with no explicit bump. run 35523207325. (task 1002620)
|
|
2303
2303
|
1.19.914 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2304
2304
|
landed since 1.19.913 with no explicit bump. run 35523700531. (task 1002620)
|
|
2305
|
+
1.19.915 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2306
|
+
landed since 1.19.914 with no explicit bump. run 35524223153. (task 1002620)
|
|
2307
|
+
1.19.916 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2308
|
+
landed since 1.19.915 with no explicit bump. run 35525694524. (task 1002620)
|
|
2305
2309
|
---------------------------------------------------------------------------
|
|
2306
2310
|
```
|
|
@@ -171,7 +171,7 @@ async function emitItemClosed(closed, { cause, votes } = {}) {
|
|
|
171
171
|
const FULL_IDEA_SECTION_KEYS = Object.freeze(['purpose', 'big_picture', 'why_had', 'why_matters', 'fit']);
|
|
172
172
|
|
|
173
173
|
// Author karma on RATIFICATION: +3, mirroring the criterion-proposal ratify
|
|
174
|
-
// award (modules/ideas/
|
|
174
|
+
// award (modules/ideas/ratify.js KARMA_RATIFY_DELTA — tuned down from +5 in
|
|
175
175
|
// V3.R28; the +5 still printed in planning-session/SKILL.md and openapi.json is
|
|
176
176
|
// STALE, do not copy it). Ratification is the CLOSE outcome, so R10's tally is
|
|
177
177
|
// what fires the payout — this file owns the constant and the payout so the
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/ideas/config.js — the two PER-INSTANCE IDEATOR DIALS (BV2.R05, spec
|
|
4
|
+
// decisions 3 and 16).
|
|
5
|
+
//
|
|
6
|
+
// The owner's ruling is that both of these belong to the PROJECT, not to the
|
|
7
|
+
// platform: a project of one wants every thought claimable, a project of fifty
|
|
8
|
+
// wants a nod. They live in ONE home, deliberately, so that the second dial did
|
|
9
|
+
// not grow a separate one — which is the whole point of R05 scoping them
|
|
10
|
+
// together rather than as two tasks.
|
|
11
|
+
//
|
|
12
|
+
// Precedence mirrors modules/government/config.js, which mirrors src/branding.js:
|
|
13
|
+
//
|
|
14
|
+
// env override -> instance config (config/ideas.json) -> neutral starter
|
|
15
|
+
// (config/ideas.neutral.json)
|
|
16
|
+
//
|
|
17
|
+
// A fresh instance with NO config resolves to the neutral starter, and the
|
|
18
|
+
// neutral starter is chosen so that resolving it changes NOTHING about today's
|
|
19
|
+
// behaviour. That is the fail-to-vanilla posture branding and government already
|
|
20
|
+
// take, and it is what lets this land ahead of its consumers.
|
|
21
|
+
//
|
|
22
|
+
// ── WHAT THIS FILE IS NOT ────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// It is NOT an authority surface, and the ignite floor is NOT the trusted tier.
|
|
25
|
+
//
|
|
26
|
+
// modules/ideas/routing.js spells `METIC_PLUS` once but READS it for four
|
|
27
|
+
// semantically different gates:
|
|
28
|
+
//
|
|
29
|
+
// 1. decideLanding, homeless-bug branch — ready vs backlog
|
|
30
|
+
// 2. decideLanding, goal-carrying branch — ready vs backlog
|
|
31
|
+
// 3. mayProposeCriterion — who may FILE a criterion-proposal
|
|
32
|
+
// 4. the protected-scope wall — who may route into the
|
|
33
|
+
// permission / ship / deploy core
|
|
34
|
+
//
|
|
35
|
+
// ONLY (1) and (2) are the ignite floor, and only they read this dial. Gates (3)
|
|
36
|
+
// and (4) stay pinned to the fixed Metic+ constant, because an instance lowering
|
|
37
|
+
// its ignite floor to `xenos` — exactly the "project of one" case the owner
|
|
38
|
+
// asked for — must not thereby gain the ability to file criterion-proposals or
|
|
39
|
+
// to put rows into the protected core's queue. routing.js says this itself at
|
|
40
|
+
// the constant's definition ("It grants nothing — the server-side rank floor and
|
|
41
|
+
// the goal's scope wall are separate, load-bearing gates"), the protected-scope
|
|
42
|
+
// branch says "the two are layers, not alternatives", and
|
|
43
|
+
// tests/filing_abuse_matrix.mjs models the widening as an ATTACK: its header
|
|
44
|
+
// records "METIC_PLUS widened to include xenos ... 4 of 23 fail".
|
|
45
|
+
//
|
|
46
|
+
// So the dial below decides one thing only: whether a filed bug lands CLAIMABLE
|
|
47
|
+
// or lands AWAITING A NOD. Both landings create the same row, in the same goal,
|
|
48
|
+
// with the same rank floor derived by createTask; the difference is a status a
|
|
49
|
+
// human can flip either way. That is a scheduling preference, which is why it is
|
|
50
|
+
// safe to configure — and it is why nothing else in this module reads it.
|
|
51
|
+
//
|
|
52
|
+
// ── THE DECAY DIAL'S SLICE BOUNDARY ──────────────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// `descentDecayRate` has NO production consumer in this commit. R11 (the ideator
|
|
55
|
+
// payout following the descent) is its intended reader and has not landed — it
|
|
56
|
+
// depends on R08, which is not built. Shipping the contract ahead of the caller
|
|
57
|
+
// is the same deliberate slice boundary modules/government/config.js documents
|
|
58
|
+
// for itself ("deliberately has no production caller yet ... the tests pin the
|
|
59
|
+
// contract until they land"), and it is what decision 16 asked for when it said
|
|
60
|
+
// the two dials should share one home "rather than growing separately".
|
|
61
|
+
//
|
|
62
|
+
// Landing it now is the cheap half. Landing it with R11 would mean R11 either
|
|
63
|
+
// inventing a second home or retrofitting this one mid-build.
|
|
64
|
+
|
|
65
|
+
const fs = require('node:fs');
|
|
66
|
+
const path = require('node:path');
|
|
67
|
+
const api = require('../../src/module-api');
|
|
68
|
+
|
|
69
|
+
// THE TWO FILES COME FROM TWO DIFFERENT ROOTS (ADR 0268, task 1003739). Under a
|
|
70
|
+
// PINNED core, __dirname is inside node_modules/@cloudbongos/core, so resolving
|
|
71
|
+
// both from one root reads the INSTANCE pack out of node_modules — a path no
|
|
72
|
+
// host authors and every `npm ci` erases. Core content from resolveCoreRoot(),
|
|
73
|
+
// host content from resolveInstanceRoot(), reached through the module doorway.
|
|
74
|
+
//
|
|
75
|
+
// Resolved at load rather than per call, matching government/config.js: a lazy
|
|
76
|
+
// resolveInstanceRoot() falls back to process.cwd(), so a chdir mid-process
|
|
77
|
+
// would move the config file underneath a running server.
|
|
78
|
+
const CORE_ROOT = api.resolveCoreRoot();
|
|
79
|
+
const INSTANCE_ROOT = api.resolveInstanceRoot();
|
|
80
|
+
const NEUTRAL_PATH = path.join(CORE_ROOT, 'config', 'ideas.neutral.json');
|
|
81
|
+
const INSTANCE_PATH = path.join(INSTANCE_ROOT, 'config', 'ideas.json');
|
|
82
|
+
|
|
83
|
+
// The rank ladder, lowest authority first, DERIVED from the doorway rather than
|
|
84
|
+
// restated. `api.LIVE_RANK_LADDER` is the single source of truth, and
|
|
85
|
+
// tests/rank_tier_single_source.mjs enforces that: a literal ladder array in any
|
|
86
|
+
// tracked src/modules/scripts file fails CI unless it is allowlisted with a
|
|
87
|
+
// pinning assertion beside it. Deriving is better than restating here anyway —
|
|
88
|
+
// it removes the drift risk instead of merely detecting it, and adding a rank
|
|
89
|
+
// widens this floor's vocabulary automatically.
|
|
90
|
+
//
|
|
91
|
+
// It is a lazy getter on the doorway, so reading it at load costs a require of
|
|
92
|
+
// the kernel this module's callers already load; it opens no connection.
|
|
93
|
+
const RANK_ORDER = Object.freeze([...api.LIVE_RANK_LADDER]);
|
|
94
|
+
|
|
95
|
+
// The defaults, as code. They are ALSO in config/ideas.neutral.json, and that is
|
|
96
|
+
// not duplication for its own sake: the neutral file is what a host copies and
|
|
97
|
+
// edits, and these are what the resolver falls back to when the neutral file is
|
|
98
|
+
// missing, unreadable or malformed. A core whose config/ directory did not ship
|
|
99
|
+
// must still boot to today's behaviour rather than to zero.
|
|
100
|
+
const DEFAULT_IGNITE_FLOOR = 'metic';
|
|
101
|
+
const DEFAULT_DECAY_RATE = 0.5;
|
|
102
|
+
|
|
103
|
+
// Read a JSON pack off disk. A MISSING file is not an error — it is the ordinary
|
|
104
|
+
// case for config/ideas.json, which most instances never author. A file that
|
|
105
|
+
// exists but does not parse returns null and is skipped, so a typo in an
|
|
106
|
+
// instance pack falls back to the neutral defaults rather than bricking boot.
|
|
107
|
+
// That is the same "fail to vanilla, do not fail to dead" posture as the rest of
|
|
108
|
+
// the config layer.
|
|
109
|
+
function readPack(file) {
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A floor is valid only if it NAMES A RANK. Case is normalised but whitespace is
|
|
125
|
+
// NOT trimmed — 'METIC ' is a hostile shape, not a rank, the same judgment
|
|
126
|
+
// mayProposeCriterion already records. A non-string, an unknown rank or an empty
|
|
127
|
+
// value falls back to the default rather than being coerced, because a floor
|
|
128
|
+
// that silently became `xenos` through a typo is precisely the widening this
|
|
129
|
+
// whole file exists to prevent.
|
|
130
|
+
function normaliseFloor(value) {
|
|
131
|
+
if (typeof value !== 'string') return null;
|
|
132
|
+
const lower = value.toLowerCase();
|
|
133
|
+
return RANK_ORDER.includes(lower) ? lower : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The decay rate is a SHARE, so it lives in [0, 1]. 0 means a descendant's ship
|
|
137
|
+
// pays its origin nothing beyond the first hop; 1 means no decay at all, every
|
|
138
|
+
// hop paying in full. Both ends are legitimate project choices, so both are
|
|
139
|
+
// admitted — what is refused is a value outside the band, a non-finite number
|
|
140
|
+
// (NaN, Infinity) or a non-number, each of which falls back to the default.
|
|
141
|
+
// Refusing rather than clamping is deliberate: a config that asked for 1.5 has
|
|
142
|
+
// asked for something incoherent, and silently paying 100% would be a payout bug
|
|
143
|
+
// wearing a config file's clothes.
|
|
144
|
+
function normaliseDecayRate(value) {
|
|
145
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
146
|
+
return value >= 0 && value <= 1 ? value : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The env layer. It exists for the co-tenant case branding's AUTH_IDP override
|
|
150
|
+
// exists for — several hosted instances sharing one neutral pack, differentiated
|
|
151
|
+
// only by env, with no config file of their own. Unlike government's, where the
|
|
152
|
+
// env layer is deliberately labels-only because an env var must never widen
|
|
153
|
+
// authority, BOTH dials are exposed here: neither one grants any authority (see
|
|
154
|
+
// the header), so neither is an authority knob.
|
|
155
|
+
function applyEnvOverrides(resolved, env) {
|
|
156
|
+
const out = { ...resolved };
|
|
157
|
+
|
|
158
|
+
const floorRaw = api.resolveEnv('IDEAS_IGNITE_FLOOR', { env });
|
|
159
|
+
const floor = normaliseFloor(floorRaw);
|
|
160
|
+
if (floor) out.igniteFloor = floor;
|
|
161
|
+
|
|
162
|
+
const rateRaw = api.resolveEnv('IDEAS_DESCENT_DECAY_RATE', { env });
|
|
163
|
+
if (rateRaw != null && String(rateRaw).trim() !== '') {
|
|
164
|
+
const rate = normaliseDecayRate(Number(rateRaw));
|
|
165
|
+
if (rate != null) out.descentDecayRate = rate;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// loadIdeasConfig — resolve the dials. Never throws: every layer that cannot be
|
|
172
|
+
// read or does not validate is skipped in favour of the one beneath it, and the
|
|
173
|
+
// bottom layer is the in-code defaults. Pass `env` and `roots` to test without
|
|
174
|
+
// touching the real filesystem or process.env.
|
|
175
|
+
function loadIdeasConfig({ env = process.env, roots = null } = {}) {
|
|
176
|
+
const neutralPath = roots && roots.neutralPath ? roots.neutralPath : NEUTRAL_PATH;
|
|
177
|
+
const instancePath = roots && roots.instancePath ? roots.instancePath : INSTANCE_PATH;
|
|
178
|
+
|
|
179
|
+
const neutral = readPack(neutralPath) || {};
|
|
180
|
+
const instance = readPack(instancePath) || {};
|
|
181
|
+
|
|
182
|
+
const resolved = {
|
|
183
|
+
igniteFloor:
|
|
184
|
+
normaliseFloor(instance.igniteFloor)
|
|
185
|
+
|| normaliseFloor(neutral.igniteFloor)
|
|
186
|
+
|| DEFAULT_IGNITE_FLOOR,
|
|
187
|
+
descentDecayRate:
|
|
188
|
+
normaliseDecayRate(instance.descentDecayRate)
|
|
189
|
+
?? normaliseDecayRate(neutral.descentDecayRate)
|
|
190
|
+
?? DEFAULT_DECAY_RATE,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return Object.freeze(applyEnvOverrides(resolved, env));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// trustedRanksForFloor — turn a floor into the SET decideLanding actually asks.
|
|
197
|
+
//
|
|
198
|
+
// A floor is a point on the ladder and admits everything at or above it, so
|
|
199
|
+
// 'metic' yields {metic, archon} — byte-for-byte the behaviour of the METIC_PLUS
|
|
200
|
+
// constant, which is what makes `igniteFloor: 'metic'` a true no-op default.
|
|
201
|
+
// An unrecognised floor yields the default's set rather than an empty one:
|
|
202
|
+
// failing closed here would mean NOBODY could ignite, turning a config typo into
|
|
203
|
+
// a silent queue stoppage.
|
|
204
|
+
function trustedRanksForFloor(floor) {
|
|
205
|
+
const at = RANK_ORDER.indexOf(normaliseFloor(floor) || DEFAULT_IGNITE_FLOOR);
|
|
206
|
+
const from = at === -1 ? RANK_ORDER.indexOf(DEFAULT_IGNITE_FLOOR) : at;
|
|
207
|
+
return Object.freeze(new Set(RANK_ORDER.slice(from)));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The resolved dials, read ONCE at load. Config is a file on disk that does not
|
|
211
|
+
// change under a running process; re-reading it per filing would put a
|
|
212
|
+
// synchronous fs call on the capture path for a value that cannot have moved.
|
|
213
|
+
const CONFIG = loadIdeasConfig();
|
|
214
|
+
|
|
215
|
+
// The set decideLanding's two landing branches consume, derived once from the
|
|
216
|
+
// resolved floor for the same reason.
|
|
217
|
+
const TRUSTED_FILER_RANKS = trustedRanksForFloor(CONFIG.igniteFloor);
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
loadIdeasConfig,
|
|
221
|
+
trustedRanksForFloor,
|
|
222
|
+
igniteFloor: CONFIG.igniteFloor,
|
|
223
|
+
descentDecayRate: CONFIG.descentDecayRate,
|
|
224
|
+
TRUSTED_FILER_RANKS,
|
|
225
|
+
RANK_ORDER,
|
|
226
|
+
DEFAULT_IGNITE_FLOOR,
|
|
227
|
+
DEFAULT_DECAY_RATE,
|
|
228
|
+
NEUTRAL_PATH,
|
|
229
|
+
INSTANCE_PATH,
|
|
230
|
+
};
|