@entelligentsia/forgecli 0.11.3 → 0.15.0

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/README.md +2 -1
  3. package/dist/CHANGELOG-forge-plugin.md +183 -0
  4. package/dist/bin/forge.js +20 -1
  5. package/dist/bin/forge.js.map +1 -1
  6. package/dist/extensions/forgecli/config-layer.d.ts +15 -0
  7. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  8. package/dist/extensions/forgecli/enhance.js +1 -1
  9. package/dist/extensions/forgecli/enhance.js.map +1 -1
  10. package/dist/extensions/forgecli/forge-cli-schema.json +19 -0
  11. package/dist/extensions/forgecli/forge-tools.js +80 -0
  12. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-update-command.js +24 -18
  14. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  15. package/dist/extensions/forgecli/friction-emit.d.ts +97 -0
  16. package/dist/extensions/forgecli/friction-emit.js +246 -0
  17. package/dist/extensions/forgecli/friction-emit.js.map +1 -0
  18. package/dist/extensions/forgecli/hook-dispatcher.js +20 -0
  19. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  20. package/dist/extensions/forgecli/index.js +29 -5
  21. package/dist/extensions/forgecli/index.js.map +1 -1
  22. package/dist/extensions/forgecli/regenerate.d.ts +22 -0
  23. package/dist/extensions/forgecli/regenerate.js +133 -3
  24. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  25. package/dist/extensions/forgecli/skill-curation-flag.d.ts +21 -0
  26. package/dist/extensions/forgecli/skill-curation-flag.js +71 -0
  27. package/dist/extensions/forgecli/skill-curation-flag.js.map +1 -0
  28. package/dist/extensions/forgecli/skill-curator-subagent.d.ts +101 -0
  29. package/dist/extensions/forgecli/skill-curator-subagent.js +342 -0
  30. package/dist/extensions/forgecli/skill-curator-subagent.js.map +1 -0
  31. package/dist/extensions/forgecli/skill-retriever.d.ts +84 -0
  32. package/dist/extensions/forgecli/skill-retriever.js +246 -0
  33. package/dist/extensions/forgecli/skill-retriever.js.map +1 -0
  34. package/dist/extensions/forgecli/skill-usage-tracker.d.ts +91 -0
  35. package/dist/extensions/forgecli/skill-usage-tracker.js +224 -0
  36. package/dist/extensions/forgecli/skill-usage-tracker.js.map +1 -0
  37. package/dist/forge-payload/.base-pack/workflows/enhance.md +331 -11
  38. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  39. package/dist/forge-payload/.schemas/event.schema.json +20 -2
  40. package/dist/forge-payload/.schemas/migrations.json +96 -0
  41. package/dist/forge-payload/.schemas/proposal.schema.json +40 -0
  42. package/dist/forge-payload/agents/store-query-validator.md +103 -0
  43. package/dist/forge-payload/agents/tomoshibi.md +185 -0
  44. package/dist/forge-payload/commands/regenerate.md +109 -20
  45. package/dist/forge-payload/hooks/check-update.js +378 -0
  46. package/dist/forge-payload/hooks/forge-permissions.js +158 -0
  47. package/dist/forge-payload/hooks/triage-error.js +71 -0
  48. package/dist/forge-payload/hooks/validate-write.js +236 -0
  49. package/dist/forge-payload/integrity.json +32 -0
  50. package/dist/forge-payload/meta/workflows/meta-enhance.md +331 -11
  51. package/dist/forge-payload/schemas/structure-manifest.json +511 -0
  52. package/dist/forge-payload/tools/compression-gate.cjs +192 -0
  53. package/dist/forge-payload/tools/delete-candidate-detector.cjs +114 -0
  54. package/dist/forge-payload/tools/judge-proposal.cjs +177 -0
  55. package/dist/forge-payload/tools/manage-versions.cjs +132 -4
  56. package/dist/forge-payload/tools/queue-drain.cjs +152 -0
  57. package/dist/forge-payload/tools/replay-scoring.cjs +117 -0
  58. package/node_modules/@mariozechner/clipboard/package.json +2 -1
  59. package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +3 -0
  60. package/node_modules/@mariozechner/clipboard-linux-x64-musl/clipboard.linux-x64-musl.node +0 -0
  61. package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +25 -0
  62. package/package.json +4 -2
@@ -0,0 +1,511 @@
1
+ {
2
+ "version": "0.46.1",
3
+ "generatedAt": "2026-05-22T02:09:22.016Z",
4
+ "generatedByTool": "build-manifest.cjs",
5
+ "namespaces": {
6
+ "personas": {
7
+ "logicalKey": "personas",
8
+ "dir": ".forge/personas",
9
+ "files": [
10
+ "architect.md",
11
+ "bug-fixer.md",
12
+ "collator.md",
13
+ "engineer.md",
14
+ "qa-engineer.md",
15
+ "supervisor.md"
16
+ ]
17
+ },
18
+ "skills": {
19
+ "logicalKey": "skills",
20
+ "dir": ".forge/skills",
21
+ "files": [
22
+ "architect-skills.md",
23
+ "bug-fixer-skills.md",
24
+ "collator-skills.md",
25
+ "engineer-skills.md",
26
+ "generic-skills.md",
27
+ "qa-engineer-skills.md",
28
+ "supervisor-skills.md"
29
+ ]
30
+ },
31
+ "workflows": {
32
+ "logicalKey": "workflows",
33
+ "dir": ".forge/workflows",
34
+ "files": [
35
+ "architect_approve.md",
36
+ "architect_review_sprint_completion.md",
37
+ "architect_sprint_intake.md",
38
+ "architect_sprint_plan.md",
39
+ "collator_agent.md",
40
+ "commit_task.md",
41
+ "fix_bug.md",
42
+ "implement_plan.md",
43
+ "migrate_structural.md",
44
+ "orchestrate_task.md",
45
+ "plan_task.md",
46
+ "quiz_agent.md",
47
+ "review_code.md",
48
+ "review_plan.md",
49
+ "run_sprint.md",
50
+ "sprint_retrospective.md",
51
+ "update_implementation.md",
52
+ "update_plan.md",
53
+ "validate_task.md"
54
+ ]
55
+ },
56
+ "templates": {
57
+ "logicalKey": "templates",
58
+ "dir": ".forge/templates",
59
+ "files": [
60
+ "CODE_REVIEW_TEMPLATE.md",
61
+ "COST_REPORT_TEMPLATE.md",
62
+ "PLAN_REVIEW_TEMPLATE.md",
63
+ "PLAN_SUMMARY_TEMPLATE.json",
64
+ "PLAN_TEMPLATE.md",
65
+ "PROGRESS_TEMPLATE.md",
66
+ "RETROSPECTIVE_TEMPLATE.md",
67
+ "SPRINT_MANIFEST_TEMPLATE.md",
68
+ "SPRINT_REQUIREMENTS_TEMPLATE.md",
69
+ "TASK_PROMPT_TEMPLATE.md"
70
+ ]
71
+ },
72
+ "commands": {
73
+ "logicalKey": "commands",
74
+ "dir": ".claude/commands",
75
+ "prefixed": true,
76
+ "files": [
77
+ "approve.md",
78
+ "collate.md",
79
+ "commit.md",
80
+ "enhance.md",
81
+ "fix-bug.md",
82
+ "implement.md",
83
+ "plan.md",
84
+ "quiz-agent.md",
85
+ "retrospective.md",
86
+ "review-code.md",
87
+ "review-plan.md",
88
+ "run-sprint.md",
89
+ "run-task.md",
90
+ "sprint-intake.md",
91
+ "sprint-plan.md",
92
+ "validate.md"
93
+ ]
94
+ },
95
+ "fragments": {
96
+ "logicalKey": "fragments",
97
+ "dir": ".forge/workflows/_fragments",
98
+ "files": [
99
+ "context-injection.md",
100
+ "event-emission-schema.md",
101
+ "finalize.md",
102
+ "friction-emit.md",
103
+ "progress-reporting.md",
104
+ "store-cli-verbs.md"
105
+ ]
106
+ },
107
+ "schemas": {
108
+ "logicalKey": "schemas",
109
+ "dir": ".forge/schemas",
110
+ "files": [
111
+ "bug.schema.json",
112
+ "collation-state.schema.json",
113
+ "config.schema.json",
114
+ "event-sidecar.schema.json",
115
+ "event.schema.json",
116
+ "feature.schema.json",
117
+ "progress-entry.schema.json",
118
+ "project-context.schema.json",
119
+ "project-overlay.schema.json",
120
+ "proposal.schema.json",
121
+ "sprint.schema.json",
122
+ "structure-versions.schema.json",
123
+ "task.schema.json"
124
+ ]
125
+ }
126
+ },
127
+ "edges": {
128
+ "workflows": {
129
+ "architect_approve": {
130
+ "personas": [
131
+ ".forge/personas/architect.md"
132
+ ],
133
+ "skills": [
134
+ ".forge/skills/architect-skills.md",
135
+ ".forge/skills/generic-skills.md"
136
+ ],
137
+ "templates": [],
138
+ "sub_workflows": [],
139
+ "kb_docs": [
140
+ "{KB_PATH}/architecture/stack.md"
141
+ ],
142
+ "config_fields": [
143
+ "paths.engineering"
144
+ ]
145
+ },
146
+ "collator_agent": {
147
+ "personas": [
148
+ ".forge/personas/collator.md"
149
+ ],
150
+ "skills": [
151
+ ".forge/skills/collator-skills.md",
152
+ ".forge/skills/generic-skills.md"
153
+ ],
154
+ "templates": [],
155
+ "sub_workflows": [],
156
+ "kb_docs": [
157
+ "{KB_PATH}/MASTER_INDEX.md"
158
+ ],
159
+ "config_fields": [
160
+ "paths.engineering"
161
+ ]
162
+ },
163
+ "commit_task": {
164
+ "personas": [
165
+ ".forge/personas/engineer.md"
166
+ ],
167
+ "skills": [
168
+ ".forge/skills/engineer-skills.md",
169
+ ".forge/skills/generic-skills.md"
170
+ ],
171
+ "templates": [
172
+ ".forge/templates/PROGRESS_TEMPLATE.md"
173
+ ],
174
+ "sub_workflows": [],
175
+ "kb_docs": [],
176
+ "config_fields": [
177
+ "commands.test",
178
+ "paths.engineering"
179
+ ]
180
+ },
181
+ "fix_bug": {
182
+ "personas": [
183
+ ".forge/personas/bug-fixer.md",
184
+ ".forge/personas/supervisor.md",
185
+ ".forge/personas/architect.md",
186
+ ".forge/personas/engineer.md",
187
+ ".forge/personas/collator.md"
188
+ ],
189
+ "skills": [
190
+ ".forge/skills/bug-fixer-skills.md",
191
+ ".forge/skills/supervisor-skills.md",
192
+ ".forge/skills/architect-skills.md",
193
+ ".forge/skills/engineer-skills.md",
194
+ ".forge/skills/generic-skills.md"
195
+ ],
196
+ "templates": [
197
+ ".forge/templates/PROGRESS_TEMPLATE.md"
198
+ ],
199
+ "sub_workflows": [
200
+ ".forge/workflows/plan_task.md",
201
+ ".forge/workflows/implement_plan.md",
202
+ ".forge/workflows/review_plan.md",
203
+ ".forge/workflows/review_code.md",
204
+ ".forge/workflows/architect_approve.md",
205
+ ".forge/workflows/commit_task.md"
206
+ ],
207
+ "kb_docs": [
208
+ "{KB_PATH}/architecture/stack.md",
209
+ "{KB_PATH}/architecture/routing.md"
210
+ ],
211
+ "config_fields": [
212
+ "commands.test",
213
+ "paths.engineering"
214
+ ]
215
+ },
216
+ "implement_plan": {
217
+ "personas": [
218
+ ".forge/personas/engineer.md"
219
+ ],
220
+ "skills": [
221
+ ".forge/skills/engineer-skills.md",
222
+ ".forge/skills/generic-skills.md"
223
+ ],
224
+ "templates": [
225
+ ".forge/templates/PROGRESS_TEMPLATE.md"
226
+ ],
227
+ "sub_workflows": [
228
+ ".forge/workflows/review_code.md"
229
+ ],
230
+ "kb_docs": [
231
+ "{KB_PATH}/architecture/stack.md",
232
+ "{KB_PATH}/architecture/routing.md"
233
+ ],
234
+ "config_fields": [
235
+ "commands.test",
236
+ "paths.engineering"
237
+ ]
238
+ },
239
+ "orchestrate_task": {
240
+ "personas": [
241
+ ".forge/personas/architect.md",
242
+ ".forge/personas/engineer.md",
243
+ ".forge/personas/supervisor.md",
244
+ ".forge/personas/bug-fixer.md",
245
+ ".forge/personas/collator.md",
246
+ ".forge/personas/qa-engineer.md"
247
+ ],
248
+ "skills": [
249
+ ".forge/skills/architect-skills.md",
250
+ ".forge/skills/engineer-skills.md",
251
+ ".forge/skills/supervisor-skills.md",
252
+ ".forge/skills/generic-skills.md"
253
+ ],
254
+ "templates": [],
255
+ "sub_workflows": [
256
+ ".forge/workflows/plan_task.md",
257
+ ".forge/workflows/implement_plan.md",
258
+ ".forge/workflows/review_plan.md",
259
+ ".forge/workflows/review_code.md",
260
+ ".forge/workflows/fix_bug.md",
261
+ ".forge/workflows/architect_approve.md",
262
+ ".forge/workflows/commit_task.md",
263
+ ".forge/workflows/validate_task.md"
264
+ ],
265
+ "kb_docs": [
266
+ "{KB_PATH}/architecture/stack.md"
267
+ ],
268
+ "config_fields": [
269
+ "paths.engineering"
270
+ ]
271
+ },
272
+ "plan_task": {
273
+ "personas": [
274
+ ".forge/personas/architect.md"
275
+ ],
276
+ "skills": [
277
+ ".forge/skills/architect-skills.md",
278
+ ".forge/skills/generic-skills.md"
279
+ ],
280
+ "templates": [
281
+ ".forge/templates/PLAN_TEMPLATE.md",
282
+ ".forge/templates/TASK_PROMPT_TEMPLATE.md"
283
+ ],
284
+ "sub_workflows": [
285
+ ".forge/workflows/review_plan.md"
286
+ ],
287
+ "kb_docs": [
288
+ "{KB_PATH}/architecture/stack.md"
289
+ ],
290
+ "config_fields": [
291
+ "commands.test",
292
+ "paths.engineering"
293
+ ]
294
+ },
295
+ "sprint_retrospective": {
296
+ "personas": [
297
+ ".forge/personas/architect.md"
298
+ ],
299
+ "skills": [
300
+ ".forge/skills/architect-skills.md",
301
+ ".forge/skills/generic-skills.md"
302
+ ],
303
+ "templates": [
304
+ ".forge/templates/RETROSPECTIVE_TEMPLATE.md"
305
+ ],
306
+ "sub_workflows": [],
307
+ "kb_docs": [
308
+ "{KB_PATH}/architecture/stack.md"
309
+ ],
310
+ "config_fields": [
311
+ "paths.engineering"
312
+ ]
313
+ },
314
+ "review_code": {
315
+ "personas": [
316
+ ".forge/personas/supervisor.md"
317
+ ],
318
+ "skills": [
319
+ ".forge/skills/supervisor-skills.md",
320
+ ".forge/skills/generic-skills.md"
321
+ ],
322
+ "templates": [
323
+ ".forge/templates/CODE_REVIEW_TEMPLATE.md"
324
+ ],
325
+ "sub_workflows": [],
326
+ "kb_docs": [
327
+ "{KB_PATH}/architecture/stack.md",
328
+ "{KB_PATH}/architecture/routing.md"
329
+ ],
330
+ "config_fields": [
331
+ "commands.test",
332
+ "paths.engineering"
333
+ ]
334
+ },
335
+ "review_plan": {
336
+ "personas": [
337
+ ".forge/personas/supervisor.md"
338
+ ],
339
+ "skills": [
340
+ ".forge/skills/supervisor-skills.md",
341
+ ".forge/skills/generic-skills.md"
342
+ ],
343
+ "templates": [
344
+ ".forge/templates/PLAN_REVIEW_TEMPLATE.md"
345
+ ],
346
+ "sub_workflows": [],
347
+ "kb_docs": [
348
+ "{KB_PATH}/architecture/stack.md"
349
+ ],
350
+ "config_fields": [
351
+ "paths.engineering"
352
+ ]
353
+ },
354
+ "architect_review_sprint_completion": {
355
+ "personas": [
356
+ ".forge/personas/architect.md"
357
+ ],
358
+ "skills": [
359
+ ".forge/skills/architect-skills.md",
360
+ ".forge/skills/generic-skills.md"
361
+ ],
362
+ "templates": [],
363
+ "sub_workflows": [],
364
+ "kb_docs": [
365
+ "{KB_PATH}/architecture/stack.md"
366
+ ],
367
+ "config_fields": [
368
+ "paths.engineering"
369
+ ]
370
+ },
371
+ "architect_sprint_intake": {
372
+ "personas": [
373
+ ".forge/personas/product-manager.md"
374
+ ],
375
+ "skills": [
376
+ ".forge/skills/architect-skills.md",
377
+ ".forge/skills/generic-skills.md"
378
+ ],
379
+ "templates": [
380
+ ".forge/templates/SPRINT_REQUIREMENTS_TEMPLATE.md",
381
+ ".forge/templates/SPRINT_MANIFEST_TEMPLATE.md"
382
+ ],
383
+ "sub_workflows": [],
384
+ "kb_docs": [
385
+ "{KB_PATH}/MASTER_INDEX.md",
386
+ "{KB_PATH}/architecture/stack.md"
387
+ ],
388
+ "config_fields": [
389
+ "project.prefix",
390
+ "paths.engineering"
391
+ ]
392
+ },
393
+ "architect_sprint_plan": {
394
+ "personas": [
395
+ ".forge/personas/architect.md"
396
+ ],
397
+ "skills": [
398
+ ".forge/skills/architect-skills.md",
399
+ ".forge/skills/generic-skills.md"
400
+ ],
401
+ "templates": [
402
+ ".forge/templates/SPRINT_MANIFEST_TEMPLATE.md",
403
+ ".forge/templates/TASK_PROMPT_TEMPLATE.md"
404
+ ],
405
+ "sub_workflows": [],
406
+ "kb_docs": [
407
+ "{KB_PATH}/MASTER_INDEX.md",
408
+ "{KB_PATH}/architecture/stack.md"
409
+ ],
410
+ "config_fields": [
411
+ "project.prefix",
412
+ "paths.engineering"
413
+ ]
414
+ },
415
+ "update_implementation": {
416
+ "personas": [
417
+ ".forge/personas/engineer.md"
418
+ ],
419
+ "skills": [
420
+ ".forge/skills/engineer-skills.md",
421
+ ".forge/skills/generic-skills.md"
422
+ ],
423
+ "templates": [
424
+ ".forge/templates/PROGRESS_TEMPLATE.md"
425
+ ],
426
+ "sub_workflows": [
427
+ ".forge/workflows/review_code.md"
428
+ ],
429
+ "kb_docs": [
430
+ "{KB_PATH}/architecture/stack.md"
431
+ ],
432
+ "config_fields": [
433
+ "commands.test",
434
+ "paths.engineering"
435
+ ]
436
+ },
437
+ "update_plan": {
438
+ "personas": [
439
+ ".forge/personas/architect.md"
440
+ ],
441
+ "skills": [
442
+ ".forge/skills/architect-skills.md",
443
+ ".forge/skills/generic-skills.md"
444
+ ],
445
+ "templates": [
446
+ ".forge/templates/PLAN_TEMPLATE.md"
447
+ ],
448
+ "sub_workflows": [
449
+ ".forge/workflows/review_plan.md"
450
+ ],
451
+ "kb_docs": [
452
+ "{KB_PATH}/architecture/stack.md"
453
+ ],
454
+ "config_fields": [
455
+ "paths.engineering"
456
+ ]
457
+ },
458
+ "validate_task": {
459
+ "personas": [
460
+ ".forge/personas/qa-engineer.md"
461
+ ],
462
+ "skills": [
463
+ ".forge/skills/qa-engineer-skills.md",
464
+ ".forge/skills/generic-skills.md"
465
+ ],
466
+ "templates": [],
467
+ "sub_workflows": [],
468
+ "kb_docs": [
469
+ "{KB_PATH}/architecture/stack.md"
470
+ ],
471
+ "config_fields": [
472
+ "commands.test",
473
+ "paths.engineering"
474
+ ]
475
+ },
476
+ "quiz_agent": {
477
+ "personas": [
478
+ ".forge/personas/qa-engineer.md"
479
+ ],
480
+ "skills": [
481
+ ".forge/skills/qa-engineer-skills.md",
482
+ ".forge/skills/generic-skills.md"
483
+ ],
484
+ "templates": [],
485
+ "sub_workflows": [],
486
+ "kb_docs": [
487
+ "{KB_PATH}/architecture/stack.md",
488
+ "{KB_PATH}/MASTER_INDEX.md"
489
+ ],
490
+ "config_fields": [
491
+ "paths.engineering"
492
+ ]
493
+ },
494
+ "migrate_structural": {
495
+ "personas": [
496
+ ".forge/personas/engineer.md"
497
+ ],
498
+ "skills": [
499
+ ".forge/skills/engineer-skills.md",
500
+ ".forge/skills/generic-skills.md"
501
+ ],
502
+ "templates": [],
503
+ "sub_workflows": [],
504
+ "kb_docs": [],
505
+ "config_fields": [
506
+ "paths.engineering"
507
+ ]
508
+ }
509
+ }
510
+ }
511
+ }
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+ // FORGE-S24-T06 — compression gate (reject >20% growth without 3+ frictions).
3
+ //
4
+ // Phase 2 of meta-enhance runs this gate BEFORE the LLM judge (T03). The gate
5
+ // is a cheap deterministic filter: an `update_skill` proposal that grows the
6
+ // target file by more than 20% (byte-wise) must be backed by at least 3
7
+ // supporting friction events. Insufficient support → reject; the judge never
8
+ // sees the proposal.
9
+ //
10
+ // Why a cheap pre-judge gate?
11
+ // - Judging is expensive (LLM call).
12
+ // - Unbounded growth of a skill body is the classic SkillOS failure mode —
13
+ // adding pages of trajectory copy-paste to "patch" a friction. Cheap to
14
+ // detect deterministically; wasteful to ask the judge to rule on.
15
+ //
16
+ // Op semantics:
17
+ // - `insert_skill`: not gated — the file doesn't exist yet, "growth" is
18
+ // undefined; the judge's body_under_2kb axis handles bloat instead.
19
+ // - `delete_skill`: not gated — deletion always shrinks.
20
+ // - `update_skill`: GATED. Growth is measured byte-wise (UTF-8) on the new
21
+ // body that would land after applying the diff. The caller resolves the
22
+ // "currentBody" and "newBody" by reading the file and applying the patch.
23
+ //
24
+ // Friction support:
25
+ // - Default: `proposal.sourceFrictionIds.length`.
26
+ // - Override: caller may supply `supportingFrictionCountFor(proposal) -> int`
27
+ // when the policy is "count frictions citing the same skill across the
28
+ // sprint" rather than "count citations on the proposal itself".
29
+ //
30
+ // Pure module: no fs access, no LLM call. Consumed by Phase 2 between
31
+ // step 5b (delete-candidate detection) and step 5c (LLM-judge gate).
32
+ //
33
+ // Exports:
34
+ // GROWTH_THRESHOLD — 0.20 (strict >, ties admit).
35
+ // MIN_SUPPORTING_FRICTIONS — 3.
36
+ // evaluateGrowth({ currentBody, newBody })
37
+ // -> { currentBytes, newBytes, growthRatio }
38
+ // evaluateProposal({ proposal, currentBody, newBody, supportingFrictionCount })
39
+ // -> { admit, reason, growthRatio, currentBytes, newBytes,
40
+ // supportingFrictionCount, threshold, minSupportingFrictions, op }
41
+ // filterProposals({ proposals, currentBodyFor, newBodyFor,
42
+ // supportingFrictionCountFor })
43
+ // -> { admitted: proposal[], rejected: { proposal, ...evaluation }[] }
44
+
45
+ const GROWTH_THRESHOLD = 0.20;
46
+ const MIN_SUPPORTING_FRICTIONS = 3;
47
+
48
+ const VALID_OPS = new Set(['insert_skill', 'update_skill', 'delete_skill']);
49
+
50
+ function evaluateGrowth({ currentBody, newBody }) {
51
+ if (typeof currentBody !== 'string') {
52
+ throw new TypeError('currentBody must be a string');
53
+ }
54
+ if (typeof newBody !== 'string') {
55
+ throw new TypeError('newBody must be a string');
56
+ }
57
+ const currentBytes = Buffer.byteLength(currentBody, 'utf8');
58
+ const newBytes = Buffer.byteLength(newBody, 'utf8');
59
+ // When currentBytes === 0 the ratio is undefined for update; we return
60
+ // Infinity and let the caller decide. evaluateProposal treats Infinity as
61
+ // "over threshold" — an update on an empty file is, by definition,
62
+ // unbounded growth — so the friction-support gate still applies.
63
+ const growthRatio = currentBytes === 0
64
+ ? (newBytes === 0 ? 0 : Infinity)
65
+ : (newBytes - currentBytes) / currentBytes;
66
+ return { currentBytes, newBytes, growthRatio };
67
+ }
68
+
69
+ function evaluateProposal({
70
+ proposal,
71
+ currentBody,
72
+ newBody,
73
+ supportingFrictionCount,
74
+ }) {
75
+ if (!proposal || typeof proposal !== 'object') {
76
+ throw new TypeError('proposal must be an object');
77
+ }
78
+ if (typeof proposal.op !== 'string' || !VALID_OPS.has(proposal.op)) {
79
+ throw new TypeError(
80
+ `proposal.op must be one of ${Array.from(VALID_OPS).join(', ')}; got ${JSON.stringify(proposal.op)}`,
81
+ );
82
+ }
83
+ if (typeof currentBody !== 'string') {
84
+ throw new TypeError('currentBody must be a string');
85
+ }
86
+ if (typeof newBody !== 'string') {
87
+ throw new TypeError('newBody must be a string');
88
+ }
89
+
90
+ // Resolve supporting friction count — explicit > proposal.sourceFrictionIds.
91
+ let frictionCount;
92
+ if (supportingFrictionCount === undefined) {
93
+ frictionCount = Array.isArray(proposal.sourceFrictionIds)
94
+ ? proposal.sourceFrictionIds.length
95
+ : 0;
96
+ } else {
97
+ if (!Number.isInteger(supportingFrictionCount) || supportingFrictionCount < 0) {
98
+ throw new TypeError(
99
+ 'supportingFrictionCount must be a non-negative integer',
100
+ );
101
+ }
102
+ frictionCount = supportingFrictionCount;
103
+ }
104
+
105
+ const { currentBytes, newBytes, growthRatio } = evaluateGrowth({
106
+ currentBody, newBody,
107
+ });
108
+
109
+ const base = {
110
+ growthRatio,
111
+ currentBytes,
112
+ newBytes,
113
+ supportingFrictionCount: frictionCount,
114
+ threshold: GROWTH_THRESHOLD,
115
+ minSupportingFrictions: MIN_SUPPORTING_FRICTIONS,
116
+ op: proposal.op,
117
+ };
118
+
119
+ // Non-update ops are not gated.
120
+ if (proposal.op !== 'update_skill') {
121
+ return { admit: true, reason: 'op_not_gated', ...base };
122
+ }
123
+
124
+ // Strict >: growth exactly at threshold admits.
125
+ if (!(growthRatio > GROWTH_THRESHOLD)) {
126
+ return { admit: true, reason: 'admitted_below_threshold', ...base };
127
+ }
128
+
129
+ // Over threshold — admit only if enough friction support.
130
+ if (frictionCount >= MIN_SUPPORTING_FRICTIONS) {
131
+ return { admit: true, reason: 'admitted_with_friction_support', ...base };
132
+ }
133
+
134
+ return { admit: false, reason: 'compression_gate_growth_unsupported', ...base };
135
+ }
136
+
137
+ function filterProposals({
138
+ proposals,
139
+ currentBodyFor,
140
+ newBodyFor,
141
+ supportingFrictionCountFor,
142
+ }) {
143
+ if (!Array.isArray(proposals)) {
144
+ throw new TypeError('proposals must be an array');
145
+ }
146
+ if (typeof currentBodyFor !== 'function') {
147
+ throw new TypeError('currentBodyFor must be a function');
148
+ }
149
+ if (typeof newBodyFor !== 'function') {
150
+ throw new TypeError('newBodyFor must be a function');
151
+ }
152
+ if (
153
+ supportingFrictionCountFor !== undefined &&
154
+ typeof supportingFrictionCountFor !== 'function'
155
+ ) {
156
+ throw new TypeError('supportingFrictionCountFor must be a function when provided');
157
+ }
158
+
159
+ const admitted = [];
160
+ const rejected = [];
161
+
162
+ for (const proposal of proposals) {
163
+ const currentBody = currentBodyFor(proposal);
164
+ const newBody = newBodyFor(proposal);
165
+ const supportingFrictionCount = supportingFrictionCountFor
166
+ ? supportingFrictionCountFor(proposal)
167
+ : undefined;
168
+
169
+ const evaluation = evaluateProposal({
170
+ proposal,
171
+ currentBody,
172
+ newBody,
173
+ supportingFrictionCount,
174
+ });
175
+
176
+ if (evaluation.admit) {
177
+ admitted.push(proposal);
178
+ } else {
179
+ rejected.push({ proposal, ...evaluation });
180
+ }
181
+ }
182
+
183
+ return { admitted, rejected };
184
+ }
185
+
186
+ module.exports = {
187
+ GROWTH_THRESHOLD,
188
+ MIN_SUPPORTING_FRICTIONS,
189
+ evaluateGrowth,
190
+ evaluateProposal,
191
+ filterProposals,
192
+ };