@xhulz/nina 0.28.19 → 0.28.21
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/package.json +1 -1
- package/releases/0.28.20/core/GAPS.md +125 -0
- package/releases/0.28.20/core/answered.json +29 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.28.20/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.28.20/core/tree/.claude/agents/architect.md +125 -0
- package/releases/0.28.20/core/tree/.claude/agents/dba.md +107 -0
- package/releases/0.28.20/core/tree/.claude/agents/devops.md +113 -0
- package/releases/0.28.20/core/tree/.claude/agents/implementer.md +155 -0
- package/releases/0.28.20/core/tree/.claude/agents/integration-tester.md +160 -0
- package/releases/0.28.20/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.28.20/core/tree/.claude/agents/qa.md +146 -0
- package/releases/0.28.20/core/tree/.claude/agents/reviewer.md +180 -0
- package/releases/0.28.20/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.28.20/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.28.20/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.28.20/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.28.20/core/tree/.claude/graph.md +73 -0
- package/releases/0.28.20/core/tree/.claude/patterns.md +212 -0
- package/releases/0.28.20/core/tree/.claude/pills/README.md +145 -0
- package/releases/0.28.20/core/tree/.claude/pipeline.md +102 -0
- package/releases/0.28.20/core/tree/.claude/retrieval.md +104 -0
- package/releases/0.28.20/core/tree/.claude/router.md +201 -0
- package/releases/0.28.20/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.28.20/core/tree/CLAUDE.md +155 -0
- package/releases/0.28.20/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.28.20/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.28.20/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.28.20/core/vocabulary.json +12 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.28.20/surfaces/blockchain/tree/.claude/router.md +9 -0
- package/releases/0.28.20/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/architect.md +8 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/dba.md +84 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/devops.md +14 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/implementer.md +5 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/patterns.md +29 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.20/surfaces/db/tree/.claude/router.md +10 -0
- package/releases/0.28.20/surfaces/db/tree/CLAUDE.md +23 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/.claude/router.md +21 -0
- package/releases/0.28.20/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/patterns.md +15 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.20/surfaces/frontend/tree/.claude/router.md +17 -0
- package/releases/0.28.20/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/graph.md +9 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.28.20/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.28.20/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.28.20/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.28.20/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.28.20/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.20/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.28.20/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.20/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.20/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.28.20/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.28.21/core/GAPS.md +125 -0
- package/releases/0.28.21/core/answered.json +29 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.28.21/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.28.21/core/tree/.claude/agents/architect.md +125 -0
- package/releases/0.28.21/core/tree/.claude/agents/dba.md +107 -0
- package/releases/0.28.21/core/tree/.claude/agents/devops.md +113 -0
- package/releases/0.28.21/core/tree/.claude/agents/implementer.md +155 -0
- package/releases/0.28.21/core/tree/.claude/agents/integration-tester.md +160 -0
- package/releases/0.28.21/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.28.21/core/tree/.claude/agents/qa.md +146 -0
- package/releases/0.28.21/core/tree/.claude/agents/reviewer.md +180 -0
- package/releases/0.28.21/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.28.21/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.28.21/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.28.21/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.28.21/core/tree/.claude/graph.md +73 -0
- package/releases/0.28.21/core/tree/.claude/patterns.md +212 -0
- package/releases/0.28.21/core/tree/.claude/pills/README.md +145 -0
- package/releases/0.28.21/core/tree/.claude/pipeline.md +102 -0
- package/releases/0.28.21/core/tree/.claude/retrieval.md +104 -0
- package/releases/0.28.21/core/tree/.claude/router.md +201 -0
- package/releases/0.28.21/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.28.21/core/tree/CLAUDE.md +155 -0
- package/releases/0.28.21/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.28.21/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.28.21/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.28.21/core/vocabulary.json +12 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.28.21/surfaces/blockchain/tree/.claude/router.md +9 -0
- package/releases/0.28.21/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/architect.md +8 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/dba.md +84 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/devops.md +14 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/implementer.md +5 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/patterns.md +29 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.21/surfaces/db/tree/.claude/router.md +10 -0
- package/releases/0.28.21/surfaces/db/tree/CLAUDE.md +23 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/.claude/router.md +21 -0
- package/releases/0.28.21/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/patterns.md +15 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.21/surfaces/frontend/tree/.claude/router.md +17 -0
- package/releases/0.28.21/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/graph.md +9 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.28.21/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.28.21/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.28.21/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.28.21/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.28.21/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.21/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.28.21/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.21/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.21/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.28.21/surfaces/pii/tree/CLAUDE.md +5 -0
- package/src/commands/pipeline.mjs +2 -4
- package/src/commands/stats.mjs +83 -48
- package/src/commands/upgrade.mjs +19 -4
- package/src/look.mjs +118 -0
package/src/commands/stats.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* loop-backs it was supposed to harvest were being counted right here.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
|
|
14
15
|
import { readFile, readdir } from 'node:fs/promises';
|
|
15
16
|
import { spawnSync } from 'node:child_process';
|
|
16
17
|
import { createReadStream, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
@@ -183,18 +184,21 @@ function costReport(records) {
|
|
|
183
184
|
// The same convention as the duration column beside it: the upper middle of an even count.
|
|
184
185
|
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
|
|
185
186
|
console.log(
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
heading(
|
|
188
|
+
'cost',
|
|
189
|
+
`at API list prices of ${PRICES_AS_OF}; ${measured.length} of ${records.length} runs have a token record` +
|
|
190
|
+
(unpriced > 0 ? `, ${unpriced} on a model the price table does not know` : ''),
|
|
191
|
+
),
|
|
188
192
|
);
|
|
189
|
-
console.log(` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'median'.padStart(10)}${'total'.padStart(10)}${'share'.padStart(8)}`);
|
|
193
|
+
console.log(dim(` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'median'.padStart(10)}${'total'.padStart(10)}${'share'.padStart(8)}`));
|
|
190
194
|
for (const [role, costs] of [...byRole].sort((a, b) => b[1].reduce((x, y) => x + y, 0) - a[1].reduce((x, y) => x + y, 0))) {
|
|
191
195
|
const sum = costs.reduce((a, b) => a + b, 0);
|
|
192
196
|
console.log(
|
|
193
|
-
` ${role.
|
|
194
|
-
`${money(sum).padStart(10)}${pct(sum, total).padStart(8)}`,
|
|
197
|
+
` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(costs.length).padStart(6)}${money(median(costs)).padStart(10)}` +
|
|
198
|
+
`${bold(money(sum).padStart(10))}${pct(sum, total).padStart(8)} ${pink(bar(sum / total, 20))}`,
|
|
195
199
|
);
|
|
196
200
|
}
|
|
197
|
-
console.log(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`);
|
|
201
|
+
console.log(bold(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`));
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/**
|
|
@@ -271,7 +275,7 @@ function modelReport(records) {
|
|
|
271
275
|
const costs = runs.map((r) => (r.tokens ? costOf(r.tokens, r.usage_model) : null)).filter((c) => c !== null);
|
|
272
276
|
const dates = runs.map((r) => String(r.ts).slice(0, 10)).sort();
|
|
273
277
|
lines.push(
|
|
274
|
-
` ${
|
|
278
|
+
` ${i === 0 ? `${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}` : ''.padEnd(20)}${model.padEnd(26)}${String(runs.length).padStart(5)} run(s) ${dim(`${dates[0]} → ${dates.at(-1)}`)}` +
|
|
275
279
|
(clear.length ? ` loop-back ${pct(loops, clear.length)} of ${clear.length}` : ' no readable verdict') +
|
|
276
280
|
(costs.length ? ` median ${money(median(costs))}` : ''),
|
|
277
281
|
);
|
|
@@ -303,9 +307,9 @@ function modelReport(records) {
|
|
|
303
307
|
}
|
|
304
308
|
|
|
305
309
|
if (lines.length === 0 && drift.length === 0) return;
|
|
306
|
-
console.log('
|
|
310
|
+
console.log(heading('models', 'a stage that ran on more than one model or effort level, one line each; different weeks, not an experiment'));
|
|
307
311
|
for (const line of lines) console.log(line);
|
|
308
|
-
for (const line of drift) console.log(line);
|
|
312
|
+
for (const line of drift) console.log(amber(line));
|
|
309
313
|
}
|
|
310
314
|
|
|
311
315
|
/** The verdicts that close a pipeline cycle: the change passed its tests, reached preview, or cleared the audit. */
|
|
@@ -390,15 +394,19 @@ function proportionReport(records, stepLimit = () => null) {
|
|
|
390
394
|
}
|
|
391
395
|
if (built === 0) return;
|
|
392
396
|
console.log(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
(
|
|
397
|
+
heading(
|
|
398
|
+
'proportion',
|
|
399
|
+
`${built} cycle(s) that wrote code; a cycle closes at qa PASS, devops DEPLOYED or secops SECURE, or at the next design after code` +
|
|
400
|
+
(unreadable > 0 ? `; ${unreadable} closed by a run whose verdict could not be read` : '') +
|
|
401
|
+
(open > 0 ? `; ${open} still open when their session's record ends` : ''),
|
|
402
|
+
),
|
|
396
403
|
);
|
|
397
|
-
console.log(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`);
|
|
404
|
+
console.log(dim(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`));
|
|
398
405
|
for (const row of rows) {
|
|
399
|
-
|
|
406
|
+
const designed = `${row.designed} (${pct(row.designed, row.cycles)})`.padStart(30);
|
|
407
|
+
console.log(` ${row.label.padEnd(16)}${String(row.cycles).padStart(8)}${row.cycles ? designed : dim(designed)} ${pink(bar(row.cycles ? row.designed / row.cycles : 0, 10))}`);
|
|
400
408
|
}
|
|
401
|
-
console.log(' A shape, not a verdict: a critical path is gated in full at any size, and one spec may cover sibling steps.');
|
|
409
|
+
console.log(dim(' A shape, not a verdict: a critical path is gated in full at any size, and one spec may cover sibling steps.'));
|
|
402
410
|
|
|
403
411
|
// The one size the harness does limit: what one implementer run writes. A run re-reads the context it
|
|
404
412
|
// has built on every turn, so its cost grows faster than its size — measured over two projects, the
|
|
@@ -413,7 +421,10 @@ function proportionReport(records, stepLimit = () => null) {
|
|
|
413
421
|
const largest = over.reduce((a, b) => (b.files_touched > a.files_touched ? b : a));
|
|
414
422
|
const read = typeof largest.tokens?.read === 'number' ? `, ${Math.round(largest.tokens.read / 1e6)}M tokens read from cache` : '';
|
|
415
423
|
console.log(
|
|
416
|
-
|
|
424
|
+
note(
|
|
425
|
+
`${over.length} implementer run(s) wrote more files than one step may (${stepLimit(largest.project)}); the largest wrote ${largest.files_touched}${read}. The architect splits such a spec into steps.`,
|
|
426
|
+
'warn',
|
|
427
|
+
),
|
|
417
428
|
);
|
|
418
429
|
}
|
|
419
430
|
}
|
|
@@ -551,34 +562,53 @@ export async function stats(argv, ctx) {
|
|
|
551
562
|
const span = [records[0]?.ts, records.at(-1)?.ts].map((t) => String(t).slice(0, 10));
|
|
552
563
|
const projectCount = new Set(records.map((r) => r.project)).size;
|
|
553
564
|
console.log(
|
|
554
|
-
` ${records.length} dispatches · ${projectCount} project${projectCount === 1 ? '' : 's'} · ` +
|
|
565
|
+
` ${projectCount === 1 ? `${bold(pink(projectName(records[0].project)))} · ` : ''}${bold(records.length)} dispatches · ${projectCount} project${projectCount === 1 ? '' : 's'} · ` +
|
|
555
566
|
`${span[0]} → ${span[1]}` +
|
|
556
|
-
(here ? ' (this project; --all for every project)' : '') +
|
|
557
|
-
(skipped > 0 ? ` (${skipped} non-harness project${skipped === 1 ? '' : 's'} hidden, --all to include)` : '') +
|
|
558
|
-
(held > 0 ? ` · ${held} dispatch(es) denied by a hook, not counted as runs` : '')
|
|
559
|
-
'\n',
|
|
567
|
+
(here ? dim(' (this project; --all for every project)') : '') +
|
|
568
|
+
(skipped > 0 ? dim(` (${skipped} non-harness project${skipped === 1 ? '' : 's'} hidden, --all to include)`) : '') +
|
|
569
|
+
(held > 0 ? ` · ${held} dispatch(es) denied by a hook, not counted as runs` : ''),
|
|
560
570
|
);
|
|
571
|
+
console.log(heading('stages', 'what each ran, and what came of it'));
|
|
561
572
|
console.log(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
573
|
+
dim(
|
|
574
|
+
` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'verdict'.padStart(9)}${'loop-back'.padStart(11)}` +
|
|
575
|
+
`${'rate'.padStart(7)}${'declared'.padStart(10)}${'unreadable'.padStart(12)}` +
|
|
576
|
+
`${'median'.padStart(9)}`,
|
|
577
|
+
),
|
|
565
578
|
);
|
|
566
579
|
|
|
567
580
|
const rows = [...byRole.entries()].sort((a, b) => b[1].n - a[1].n);
|
|
581
|
+
// Each stage's runs as one bar, as long as its share of the busiest stage's: what went forward, what
|
|
582
|
+
// was sent back, and what said nothing readable (or is still running).
|
|
583
|
+
const busiest = Math.max(...rows.map(([, s]) => s.n), 1);
|
|
584
|
+
const zero = (text, n) => (n === 0 ? dim(text) : text);
|
|
568
585
|
for (const [role, s] of rows) {
|
|
586
|
+
const runsBar = stacked(
|
|
587
|
+
[
|
|
588
|
+
{ n: s.clear - s.loop, char: '█', paint: pink },
|
|
589
|
+
{ n: s.loop, char: '▓', paint: amber },
|
|
590
|
+
{ n: s.n - s.clear, char: '░', paint: dim },
|
|
591
|
+
],
|
|
592
|
+
Math.max(1, Math.round((16 * s.n) / busiest)),
|
|
593
|
+
);
|
|
594
|
+
const declared = pct(s.declared, s.done).padStart(10);
|
|
595
|
+
const unread = pct(s.unclear, s.done).padStart(12);
|
|
569
596
|
console.log(
|
|
570
|
-
`
|
|
571
|
-
`${String(s.loop).padStart(11)}${pct(s.loop, s.clear).padStart(7)}` +
|
|
572
|
-
`${
|
|
573
|
-
`${medianMin(s.durations).padStart(9)}`,
|
|
597
|
+
` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(s.n).padStart(6)}${String(s.clear).padStart(9)}` +
|
|
598
|
+
`${zero(String(s.loop).padStart(11), s.loop)}${zero(pct(s.loop, s.clear).padStart(7), s.loop)}` +
|
|
599
|
+
`${s.declared < s.done ? amber(declared) : dim(declared)}${s.unclear > 0 ? amber(unread) : dim(unread)}` +
|
|
600
|
+
`${medianMin(s.durations).padStart(9)} ${runsBar}`,
|
|
574
601
|
);
|
|
575
602
|
}
|
|
603
|
+
console.log(dim(` ${' '.repeat(86)}█ forward ▓ sent back ░ no verdict read`));
|
|
576
604
|
|
|
577
605
|
const unreadable = rows.reduce((a, [, s]) => a + s.unclear, 0);
|
|
578
606
|
const done = rows.reduce((a, [, s]) => a + s.done, 0);
|
|
607
|
+
console.log('');
|
|
579
608
|
console.log(
|
|
580
|
-
|
|
581
|
-
|
|
609
|
+
unreadable === 0
|
|
610
|
+
? note("every finished run's verdict could be read.", 'ok')
|
|
611
|
+
: note(`${pct(unreadable, done)} of finished runs report no machine-readable verdict. Those stages need a verdict token on the report's first line.`, 'warn'),
|
|
582
612
|
);
|
|
583
613
|
|
|
584
614
|
// Whether the stages that send work back say what they send back. Counted only over reports read
|
|
@@ -587,8 +617,7 @@ export async function stats(argv, ctx) {
|
|
|
587
617
|
if (named.length > 0) {
|
|
588
618
|
const withIds = named.filter((r) => r.issues > 0).length;
|
|
589
619
|
console.log(
|
|
590
|
-
|
|
591
|
-
' on the `ISSUES` line under the verdict.',
|
|
620
|
+
note(`${withIds} of ${named.length} declared loop-back(s) (${pct(withIds, named.length)}) named their issues on the \`ISSUES\` line under the verdict.`, withIds < named.length ? 'warn' : 'ok'),
|
|
592
621
|
);
|
|
593
622
|
}
|
|
594
623
|
|
|
@@ -600,9 +629,11 @@ export async function stats(argv, ctx) {
|
|
|
600
629
|
for (const r of withSkills) for (const k of r.skills) tally.set(k, (tally.get(k) ?? 0) + 1);
|
|
601
630
|
const list = [...tally].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ×${n}`);
|
|
602
631
|
console.log(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
632
|
+
note(
|
|
633
|
+
`${withSkills.length} of ${observable} runs with a readable transcript (${pct(withSkills.length, observable)}) invoked a Skill` +
|
|
634
|
+
(list.length > 0 ? ` — ${list.join(', ')}` : ' — none at all'),
|
|
635
|
+
withSkills.length === 0 ? 'warn' : 'info',
|
|
636
|
+
),
|
|
606
637
|
);
|
|
607
638
|
}
|
|
608
639
|
|
|
@@ -617,7 +648,8 @@ export async function stats(argv, ctx) {
|
|
|
617
648
|
others.reduce((a, [, o]) => a + o.clear, 0),
|
|
618
649
|
);
|
|
619
650
|
console.log(
|
|
620
|
-
|
|
651
|
+
note(
|
|
652
|
+
`${role}: ${s.loop} loop-back(s) in ${s.clear} readable verdict(s) (${pct(s.loop, s.clear)}),` +
|
|
621
653
|
` against ${elsewhere} across the other gates — check whether it still gates anything.` +
|
|
622
654
|
(s.n - s.clear > 0
|
|
623
655
|
? ` ${s.n - s.clear} further run(s) produced no readable verdict at all, so the rate may understate it.`
|
|
@@ -627,6 +659,8 @@ export async function stats(argv, ctx) {
|
|
|
627
659
|
(s.declared < s.clear
|
|
628
660
|
? ` Only ${s.declared} of the ${s.clear} were declared by the stage; the rest were inferred from its report.`
|
|
629
661
|
: ''),
|
|
662
|
+
'warn',
|
|
663
|
+
),
|
|
630
664
|
);
|
|
631
665
|
}
|
|
632
666
|
|
|
@@ -642,30 +676,31 @@ export async function stats(argv, ctx) {
|
|
|
642
676
|
const newest = pills.dates.at(-1);
|
|
643
677
|
const inWindow = pills.dates.filter((d) => d >= span[0] && d <= span[1]).length;
|
|
644
678
|
|
|
645
|
-
console.log('
|
|
679
|
+
console.log(heading('learning', 'what the loop-backs taught, and what became a rule'));
|
|
646
680
|
if (pills.total === 0) {
|
|
647
|
-
console.log(
|
|
648
|
-
` ${loopBacks} loop-back(s) and not one pill — either nothing was learned, or nothing was written down.`,
|
|
649
|
-
);
|
|
681
|
+
console.log(note(`${loopBacks} loop-back(s) and not one pill — either nothing was learned, or nothing was written down.`, loopBacks > 0 ? 'warn' : 'info'));
|
|
650
682
|
} else {
|
|
651
683
|
console.log(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
684
|
+
note(
|
|
685
|
+
// A share over 100% says nothing: the pills were written for something other than these loop-backs.
|
|
686
|
+
`${loopBacks} loop-back(s) in the window → ${inWindow} pill(s) written${inWindow <= loopBacks ? ` (${pct(inWindow, loopBacks)})` : ''}` +
|
|
687
|
+
' — every loop-back is looked at for a lesson; `nina learn` shows which roles are owed one.' +
|
|
688
|
+
(pills.undated > 0 ? ` ${pills.undated} pill(s) carry no date and cannot be placed.` : ''),
|
|
689
|
+
),
|
|
656
690
|
);
|
|
657
691
|
console.log(
|
|
658
692
|
pills.retired === 0
|
|
659
|
-
? `
|
|
660
|
-
:
|
|
693
|
+
? note(`0 of ${pills.total} pill(s) retired — no correction has ever graduated into a rule.`, 'warn')
|
|
694
|
+
: note(`${pills.retired} of ${pills.total} pill(s) retired into a rule.`, 'ok'),
|
|
661
695
|
);
|
|
662
696
|
if (newest) {
|
|
663
697
|
const since = records.filter((r) => String(r.ts).slice(0, 10) > newest).length;
|
|
664
|
-
if (since > 0) console.log(
|
|
698
|
+
if (since > 0) console.log(note(`${since} dispatch(es) since the newest pill (${newest}).`));
|
|
665
699
|
}
|
|
666
700
|
}
|
|
667
701
|
for (const name of pills.unresolved) {
|
|
668
|
-
console.log(
|
|
702
|
+
console.log(note(`${name} could not be located on disk, so its pills are not counted.`, 'warn'));
|
|
669
703
|
}
|
|
704
|
+
console.log('');
|
|
670
705
|
return 0;
|
|
671
706
|
}
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -184,12 +184,27 @@ export async function upgrade(argv, ctx) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const from = layerRootFor(ctx.root, profile.core);
|
|
187
|
+
if (from.error) {
|
|
188
|
+
console.error(` ${from.error}\n`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
// The release asked for is not one this install carries: the installed package is older than it. Said
|
|
192
|
+
// as that, with the install that fixes it. It used to read "profile pins core <version>", about a pin the
|
|
193
|
+
// profile did not hold, right after an install that had failed.
|
|
187
194
|
const onto = layerRootFor(ctx.root, to);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
if (onto.error) {
|
|
196
|
+
let installed = '?';
|
|
197
|
+
try {
|
|
198
|
+
installed = JSON.parse(await readFile(join(ctx.root, 'package.json'), 'utf8')).version;
|
|
199
|
+
} catch {
|
|
200
|
+
// An install with no manifest still says what to run.
|
|
192
201
|
}
|
|
202
|
+
console.error(
|
|
203
|
+
to === 'dev'
|
|
204
|
+
? ` ${onto.error}\n`
|
|
205
|
+
: ` ${to} is not a release the installed NINA (${installed}) carries — install it first: pnpm add -D -E @xhulz/nina@${to}\n`,
|
|
206
|
+
);
|
|
207
|
+
return 1;
|
|
193
208
|
}
|
|
194
209
|
|
|
195
210
|
const surfaces = profile.surfaces ?? [];
|
package/src/look.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a report looks on a terminal: the banner's pink for stage names and section marks, dim for what
|
|
3
|
+
* explains, amber for what needs looking at, bars for shares, and lines wrapped to the window.
|
|
4
|
+
*
|
|
5
|
+
* Only on a terminal, and never under `NO_COLOR`. Piped, a report is the same lines, uncoloured and
|
|
6
|
+
* unwrapped, so `grep` still finds a whole sentence and a test still reads it; the bars stay, since a
|
|
7
|
+
* block character reads the same anywhere.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PINK, useColor } from './banner.mjs';
|
|
11
|
+
|
|
12
|
+
/** An amber for what needs looking at, and a green for what is sound: bright enough on dark and light. */
|
|
13
|
+
const AMBER = [230, 160, 40];
|
|
14
|
+
const GREEN = [90, 180, 110];
|
|
15
|
+
|
|
16
|
+
const rgb = ([r, g, b], s) => (useColor() ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m` : String(s));
|
|
17
|
+
|
|
18
|
+
/** Text in the banner's pink: a stage's name, a section's mark. */
|
|
19
|
+
export const pink = (s) => rgb(PINK, s);
|
|
20
|
+
/** Text that explains rather than reports. */
|
|
21
|
+
export const dim = (s) => (useColor() ? `\x1b[2m${s}\x1b[0m` : String(s));
|
|
22
|
+
/** Text that carries the number a line is about. */
|
|
23
|
+
export const bold = (s) => (useColor() ? `\x1b[1m${s}\x1b[0m` : String(s));
|
|
24
|
+
/** Text that needs looking at. */
|
|
25
|
+
export const amber = (s) => rgb(AMBER, s);
|
|
26
|
+
/** Text that says something is sound. */
|
|
27
|
+
export const green = (s) => rgb(GREEN, s);
|
|
28
|
+
|
|
29
|
+
/** The width lines wrap at on a terminal; a pipe is never wrapped. */
|
|
30
|
+
const width = () => Math.max(80, Math.min(process.stdout.columns ?? 120, 140));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A section's heading: its name, and what the section is about. Printed plain it reads as it always did,
|
|
34
|
+
* "name — what"; on a terminal the name is marked and the explanation dimmed.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} name - The section.
|
|
37
|
+
* @param {string} [about] - What it covers.
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function heading(name, about = '') {
|
|
41
|
+
if (!useColor()) return `\n ${name}${about ? ` — ${about}` : ''}`;
|
|
42
|
+
const lead = ` ▌ ${name} `;
|
|
43
|
+
return `\n${pink(' ▌')} ${bold(pink(name))}${about ? ` ${dim(wrapped(`— ${about}`, lead.length, lead.length + 2))}` : ''}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A line of prose under a section, wrapped to the window on a terminal with its continuation indented
|
|
48
|
+
* under its first word, and marked: `·` for what it reports, `!` in amber for what needs looking at, `✓`
|
|
49
|
+
* in green for what is sound.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} text - The sentence.
|
|
52
|
+
* @param {'info'|'warn'|'ok'} [kind] - What it is.
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function note(text, kind = 'info') {
|
|
56
|
+
if (!useColor()) return ` ${text}`;
|
|
57
|
+
const mark = kind === 'warn' ? amber('!') : kind === 'ok' ? green('✓') : dim('·');
|
|
58
|
+
const body = wrapped(text, 6, 6);
|
|
59
|
+
return ` ${mark} ${kind === 'warn' ? amber(body) : body}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wraps text at the window's width. The first line starts `first` columns in; the rest are indented by
|
|
64
|
+
* `rest`. Words are never split.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} text - The text.
|
|
67
|
+
* @param {number} first - Where the first line starts.
|
|
68
|
+
* @param {number} rest - The indent of every other line.
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function wrapped(text, first, rest) {
|
|
72
|
+
const room = width();
|
|
73
|
+
const lines = [];
|
|
74
|
+
let line = '';
|
|
75
|
+
let at = first;
|
|
76
|
+
for (const word of text.split(' ')) {
|
|
77
|
+
if (line && at + line.length + 1 + word.length > room) {
|
|
78
|
+
lines.push(line);
|
|
79
|
+
line = word;
|
|
80
|
+
at = rest;
|
|
81
|
+
} else line = line ? `${line} ${word}` : word;
|
|
82
|
+
}
|
|
83
|
+
lines.push(line);
|
|
84
|
+
return lines.join(`\n${' '.repeat(rest)}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Eighths of a block, for the fraction a bar ends on. */
|
|
88
|
+
const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A bar as long as a share of `cells`, ending on an eighth of a block.
|
|
92
|
+
*
|
|
93
|
+
* @param {number} fraction - From 0 to 1.
|
|
94
|
+
* @param {number} cells - How long a whole one is.
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function bar(fraction, cells) {
|
|
98
|
+
const eighths = Math.round(Math.max(0, Math.min(1, fraction)) * cells * 8);
|
|
99
|
+
return `${'█'.repeat(Math.floor(eighths / 8))}${EIGHTHS[eighths % 8]}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A bar split into parts, one character per part so it reads uncoloured: `█` for what went forward, `▓`
|
|
104
|
+
* for what was sent back, `░` for what said nothing readable. Each part is at least one cell when it is
|
|
105
|
+
* not zero, so a single run sent back in a hundred still shows.
|
|
106
|
+
*
|
|
107
|
+
* @param {{n: number, char: string, paint: (s: string) => string}[]} parts - In order.
|
|
108
|
+
* @param {number} cells - How long the bar is.
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
export function stacked(parts, cells) {
|
|
112
|
+
const total = parts.reduce((a, p) => a + p.n, 0);
|
|
113
|
+
if (total === 0 || cells <= 0) return '';
|
|
114
|
+
const sizes = parts.map((p) => (p.n > 0 ? Math.max(1, Math.round((p.n / total) * cells)) : 0));
|
|
115
|
+
// Trim the rounding back to the length asked for, from the largest part.
|
|
116
|
+
while (sizes.reduce((a, b) => a + b, 0) > cells) sizes[sizes.indexOf(Math.max(...sizes))] -= 1;
|
|
117
|
+
return parts.map((p, i) => p.paint(p.char.repeat(sizes[i]))).join('');
|
|
118
|
+
}
|