@shiftleftpt/sbd-toe-mcp 0.10.3 → 0.11.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 (47) hide show
  1. package/assets/agent-guide.md +33 -0
  2. package/consumed-bundle.json +8 -8
  3. package/data/entities/proportionality.json +2 -2
  4. package/data/entities/sdlc_integration.json +2 -2
  5. package/data/publish/indexes/canonical_chunks.jsonl +4903 -4903
  6. package/data/publish/indexes/publication_manifest.json +4 -4
  7. package/data/publish/ontology/sbdtoe-ontology.yaml +1699 -21
  8. package/data/publish/overlay/framework_overlay_index.json +121 -11
  9. package/data/publish/overlay/overlay_mappings.jsonl +935 -646
  10. package/data/publish/runtime/artifact_requirements.json +218 -311
  11. package/data/publish/runtime/artifacts.json +82 -55
  12. package/data/publish/runtime/controls.json +870 -311
  13. package/data/publish/runtime/deterministic_manifest.json +2 -2
  14. package/data/publish/runtime/evidence_patterns.json +162 -162
  15. package/data/publish/runtime/requirement_control_links.json +2271 -1708
  16. package/data/publish/runtime/v1/manual_threat_mitigation.jsonl +233 -233
  17. package/data/publish/runtime/v1/v1_manifest.json +6 -4
  18. package/data/reports/run_manifest.json +4 -4
  19. package/dist/index.js +107 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/serving/affordances.d.ts +1 -0
  22. package/dist/serving/affordances.js +12 -3
  23. package/dist/serving/affordances.js.map +1 -1
  24. package/dist/serving/selection.d.ts +56 -0
  25. package/dist/serving/selection.js +289 -0
  26. package/dist/serving/selection.js.map +1 -0
  27. package/dist/tools/consult-security-requirements.d.ts +7 -0
  28. package/dist/tools/consult-security-requirements.js +38 -0
  29. package/dist/tools/consult-security-requirements.js.map +1 -1
  30. package/dist/tools/generate-sbd-toe-skill.js +7 -2
  31. package/dist/tools/generate-sbd-toe-skill.js.map +1 -1
  32. package/dist/tools/get-threat-landscape.js +36 -13
  33. package/dist/tools/get-threat-landscape.js.map +1 -1
  34. package/dist/tools/map-review-scope.d.ts +6 -0
  35. package/dist/tools/map-review-scope.js +19 -0
  36. package/dist/tools/map-review-scope.js.map +1 -1
  37. package/dist/tools/ontology-loader.d.ts +8 -0
  38. package/dist/tools/ontology-loader.js +9 -0
  39. package/dist/tools/ontology-loader.js.map +1 -1
  40. package/dist/tools/prepare-codegen-context.d.ts +625 -5
  41. package/dist/tools/prepare-codegen-context.js +1185 -81
  42. package/dist/tools/prepare-codegen-context.js.map +1 -1
  43. package/dist/tools/select-requirements.d.ts +53 -0
  44. package/dist/tools/select-requirements.js +99 -0
  45. package/dist/tools/select-requirements.js.map +1 -0
  46. package/package.json +4 -3
  47. package/prompts/sbd-toe-grounded-codegen.md +10 -0
@@ -30,8 +30,114 @@ import { RuntimeV1AssetMissingError, getG2Runtime, getV1EntityDisplayName } from
30
30
  import { getOntologyData } from "./ontology-loader.js";
31
31
  import { getRegulatoryOverlay, resolveRegulatoryFramework } from "./regulatory-overlay-loader.js";
32
32
  import { expandQueryWithAliases } from "../backend/semantic-index-gateway.js";
33
+ import { requirementCategoryOf } from "../serving/requirement-id.js";
33
34
  import { prepareCodegenAffordances } from "../serving/affordances.js";
35
+ import { runSelectionWithActivation } from "../serving/selection.js";
34
36
  const EVIDENCE_PATTERN_CAP = 25;
37
+ /**
38
+ * v2 token diet, s3 — evidence-pattern cap applied at `detail: "standard" |
39
+ * "minimal"` ON TOP of the classic cap: the dieted list is the deterministic
40
+ * PREFIX (relevance_score desc, id asc — the exact order the core already
41
+ * emits) of the classic top-{@link EVIDENCE_PATTERN_CAP} list. Never silent:
42
+ * `completeness_report` reports total/returned/capped and, when anything was
43
+ * cut, `evidence_patterns_rest` says how to retrieve the rest (same tool,
44
+ * `detail: "full"`).
45
+ */
46
+ const STANDARD_EVIDENCE_PATTERN_CAP = 10;
47
+ /**
48
+ * v2 token diet, s3b (revised per the 2026-07-05 operator ADENDA in
49
+ * agentic/planeado/v2-token-diet/EPIC.md — no top-N, no subsetting of the
50
+ * activated set): at `detail: "minimal"` the evidence cap tightens 10→5 with
51
+ * the SAME s3 mechanism (deterministic prefix, never-silent counts +
52
+ * executable rest-reference). This cap — together with the minimal
53
+ * `manual_grounding` form — is the ONLY divergence from `standard`; the
54
+ * activated scope (requirements/controls/slices/entities, with the verbatim
55
+ * published descriptions) stays COMPLETE and byte-identical to `standard`.
56
+ */
57
+ const MINIMAL_EVIDENCE_PATTERN_CAP = 5;
58
+ /**
59
+ * v2 token diet, s3c (operator ADENDA 2026-07-05, reactivated same day): at
60
+ * `detail: "ultrathin"` NO evidence pattern goes inline (cap 0) — the SAME s3
61
+ * never-silent mechanism still applies in full: `completeness_report` reports
62
+ * total/returned(=0)/capped(=total) and `evidence_patterns_rest` is the
63
+ * executable reference to the CHEAPEST level that returns them inline
64
+ * (detail="minimal" ⇒ top-5; "standard" ⇒ 10; "full" ⇒ classic 25).
65
+ */
66
+ const ULTRATHIN_EVIDENCE_PATTERN_CAP = 0;
67
+ /**
68
+ * Section → source map for the dieted encoding: each list is
69
+ * source-homogeneous BY CONSTRUCTION (the projection types hardcode a single
70
+ * source literal per list), so one entry per list reconstitutes the `source`
71
+ * of every item with no exceptions. Published as part of the
72
+ * `sbd://toe/codegen-instructions/{mode}` resource (s3 moved the verbose
73
+ * legend out of every payload); the inline `provenance_legend` keeps a
74
+ * one-line pointer.
75
+ */
76
+ const PROVENANCE_SOURCES = {
77
+ "activated_scope.requirements": "runtime_v0",
78
+ "activated_scope.controls": "runtime_v0",
79
+ "activated_scope.slices": "runtime_v1",
80
+ "activated_scope.regulatory_obligations": "overlay",
81
+ "g2_context.control_objectives": "runtime_v1",
82
+ "g2_context.mechanisms": "runtime_v1",
83
+ "g2_context.practices": "runtime_v1",
84
+ "g2_context.artifacts": "runtime_v1",
85
+ "g2_context.relations": "runtime_v1",
86
+ "g2_context.evidence_patterns": "runtime_v0",
87
+ "manual_grounding.groups": "runtime_v1",
88
+ "regulatory_overlay.frameworks": "overlay",
89
+ "regulatory_overlay.obligations": "overlay",
90
+ "regulatory_overlay.mappings": "overlay",
91
+ "regulatory_overlay.playbooks": "overlay"
92
+ };
93
+ /**
94
+ * Inline legend for `standard`/`minimal` (s3: slim pointer — the full legend,
95
+ * including the section→source table and every derivation rule of the dieted
96
+ * encoding, lives in the `sbd://toe/codegen-instructions/{mode}` resource,
97
+ * section `detail_encoding`).
98
+ */
99
+ const PROVENANCE_LEGEND = {
100
+ note: "Deduplicated encoding (detail=standard/minimal): per-item `source` fields " +
101
+ "are elided (every list is source-homogeneous), requirement `category` = " +
102
+ "requirement_id category segment (the one before the number: AUT-003→AUT, " +
103
+ "REQ-AGN-001→AGN), g2_context entity lists are grouped as " +
104
+ "{slice_id: {entity_id: name|null}}, and citations ids are referenced via " +
105
+ "ids_from payload paths. Full legend: MCP resource " +
106
+ "sbd://toe/codegen-instructions/{mode}, section detail_encoding."
107
+ };
108
+ /**
109
+ * Inline legend for `ultrathin` (s3c) — the standard/minimal legend text is
110
+ * frozen within a bundle pin (snapshots are byte-frozen; the only edit so far is
111
+ * the v1.10 category-segment wording, beta.3, bundle re-pin); ultrathin carries
112
+ * its own note with the extra cut rules. Full legend: same MCP resource, section
113
+ * `detail_encoding` (incl. the `ultrathin` entry).
114
+ */
115
+ const PROVENANCE_LEGEND_ULTRATHIN = {
116
+ note: "Deduplicated encoding (detail=ultrathin): same rules as detail=standard/" +
117
+ "minimal — per-item `source` elided, requirement `category` = " +
118
+ "requirement_id category segment (before the number), g2_context entity lists grouped as " +
119
+ "{slice_id: {entity_id: name|null}}, citations ids via ids_from payload " +
120
+ "paths — PLUS: published `description` fields elided (executable " +
121
+ "activated_scope.descriptions_ref, detail='minimal'), evidence_patterns 0 " +
122
+ "inline (counts + rest-ref in completeness_report), manual_grounding " +
123
+ "aggregate-only (total + sha + groups_ref) and completeness diagnostics " +
124
+ "as exact counts (+ ref). Nothing silently dropped. Full legend: MCP " +
125
+ "resource sbd://toe/codegen-instructions/{mode}, section detail_encoding."
126
+ };
127
+ /**
128
+ * v2 token diet, s4 — cheap turns, not fewer turns: short note (≈50 tokens)
129
+ * appended to every `standard`/`minimal` ready payload. The production
130
+ * write-test-edit loop is legitimate; what must not repeat is the cost of
131
+ * re-requesting THIS payload — an identical call returns a byte-identical
132
+ * result (deterministic, tested), so the context already in the session is
133
+ * the source for the loop. Follow-ups that genuinely need more go through
134
+ * `detail: "minimal"` or a targeted `consult_security_requirements` call —
135
+ * never a repeat of the full payload. `full` carries NO hint (byte-identical
136
+ * to the classic payload, EPIC invariant 1).
137
+ */
138
+ export const REPEAT_CALL_HINT = "Identical input returns this exact payload (deterministic) — reuse the " +
139
+ "context already received instead of re-calling; deepen via " +
140
+ "detail:'minimal' or a targeted consult_security_requirements.";
35
141
  // ---------------------------------------------------------------------------
36
142
  // Activation lexicon (small, auditable; WP6 layers semantic scoring on top)
37
143
  // ---------------------------------------------------------------------------
@@ -56,7 +162,11 @@ const VALID_CONCERNS = [
56
162
  "monitoring",
57
163
  "release",
58
164
  "deployment",
59
- "integration"
165
+ "integration",
166
+ // AI-agent / automation governance catalogue (REQ-AGN-001…004, category AGN;
167
+ // consumer contract v1.10 §1.18) — maps through the loader's concernsMap
168
+ // (`agents: ["AGN"]`, absorbed from master bc8c9189 in 0.20.0-beta.3).
169
+ "agents"
60
170
  ];
61
171
  const CONCERN_LEXICON = new Set(VALID_CONCERNS);
62
172
  /**
@@ -68,7 +178,7 @@ const TASK_TERM_TO_CONCERNS = [
68
178
  ["auth", ["auth"]],
69
179
  ["authentication", ["auth"]],
70
180
  ["authorization", ["auth"]],
71
- ["login", ["auth"]],
181
+ ["login", ["auth", "encryption"]],
72
182
  ["session", ["auth"]],
73
183
  ["jwt", ["auth"]],
74
184
  ["oauth", ["auth"]],
@@ -104,11 +214,16 @@ const TASK_TERM_TO_CONCERNS = [
104
214
  ["release", ["release"]],
105
215
  ["deploy", ["deployment", "release"]],
106
216
  ["rollback", ["release"]],
107
- ["terraform", ["iac", "deployment"]],
108
- ["ansible", ["iac", "deployment"]],
217
+ ["terraform", ["iac"]],
218
+ ["ansible", ["iac"]],
109
219
  ["kubernetes", ["deployment", "config"]],
110
220
  ["docker", ["deployment", "config"]],
111
221
  ["container", ["deployment"]],
222
+ ["ai agent", ["agents"]],
223
+ ["agentic", ["agents"]],
224
+ ["kill-switch", ["agents"]],
225
+ ["kill switch", ["agents"]],
226
+ ["autonomy level", ["agents"]],
112
227
  ["threat model", ["threat_modeling"]],
113
228
  ["stride", ["threat_modeling"]],
114
229
  ["linddun", ["threat_modeling"]],
@@ -128,6 +243,15 @@ const TASK_TERM_TO_CONCERNS = [
128
243
  ["rpc", ["integration", "api"]],
129
244
  ["webhook", ["integration", "api"]],
130
245
  ["queue", ["integration"]],
246
+ // pós-P2 2026-08-31: integração por mensageria exige registo de eventos críticos → logging.
247
+ ["message queue", ["integration", "logging"]],
248
+ // pós-P2 2026-08-31: mTLS = gestão de material criptográfico → secrets (CFG/ENC).
249
+ ["mtls", ["encryption", "integration", "secrets"]],
250
+ ["signature", ["integrity", "encryption"]],
251
+ ["signing", ["integrity"]],
252
+ ["image", ["deployment", "distribution"]],
253
+ ["spa", ["validation", "api"]],
254
+ ["frontend", ["validation"]],
131
255
  ["pubsub", ["integration"]],
132
256
  ["monitoring", ["monitoring", "logging"]],
133
257
  ["metric", ["monitoring"]],
@@ -161,8 +285,12 @@ const CONCERN_TO_V0_CATEGORIES_SUPPLEMENT = {
161
285
  threat_modeling: ["THR"],
162
286
  monitoring: ["LOG", "OPS"],
163
287
  release: ["DPL", "OPS"],
164
- deployment: ["DPL", "IAC", "CNT"],
165
- integration: ["API", "INT"]
288
+ // pós-P2 2026-08-31: deploy activa também a categoria base DST (cap. 02 — "Deploy
289
+ // apenas via pipeline validado" e afins); supplement do serving, loader inalterado.
290
+ deployment: ["DPL", "IAC", "CNT", "DST"],
291
+ integration: ["API", "INT"],
292
+ // `agents` → AGN comes from ontology.concernsMap (loader); nothing to supplement.
293
+ agents: []
166
294
  };
167
295
  const CONCERN_TO_SLICE_FAMILY = {
168
296
  auth: "ACO-IAT",
@@ -185,7 +313,10 @@ const CONCERN_TO_SLICE_FAMILY = {
185
313
  monitoring: "ACO-SLG",
186
314
  release: "ACO-RPR",
187
315
  deployment: "ACO-RPR",
188
- integration: "ACO-ITS"
316
+ integration: "ACO-ITS",
317
+ // No AppSec Core slice family for the AGN catalogue (no published control link
318
+ // for REQ-AGN-001…004 — declared gap, never invented).
319
+ agents: null
189
320
  };
190
321
  /**
191
322
  * PT codegen aliases that complement `CANONICAL_ALIASES_PT_EN` in the semantic
@@ -209,7 +340,17 @@ const CODEGEN_PT_ALIASES = [
209
340
  ["integração", ["integration"]],
210
341
  ["fronteira", ["boundary"]],
211
342
  ["arquitetura", ["architecture"]],
343
+ // R3 do ciclo MP1 (2026-08-31, crescimento por semântica do Manual — cap. 02
344
+ // categoria SES = sessões; nunca por caso do oráculo): sessão/sessões → session.
345
+ ["sessão", ["session"]],
346
+ ["sessões", ["session"]],
212
347
  ["chave de api", ["api key"]],
348
+ ["chave de cliente", ["api key"]],
349
+ ["chaves de cliente", ["api key"]],
350
+ ["mensageria", ["message queue"]],
351
+ ["fila de mensagens", ["message queue"]],
352
+ ["assinatura", ["signature"]],
353
+ ["imagem", ["image"]],
213
354
  ["variável de ambiente", ["environment variable"]]
214
355
  ];
215
356
  /**
@@ -231,6 +372,8 @@ const COMPOUND_TERM_TO_CONCERNS = [
231
372
  ["ci pipeline", ["build", "supply_chain"]],
232
373
  ["trust boundary", ["architecture"]],
233
374
  ["fronteira de confiança", ["architecture"]],
375
+ ["formulário de registo", ["auth", "validation"]],
376
+ ["registration form", ["auth", "validation"]],
234
377
  ["service to service", ["integration", "architecture"]],
235
378
  ["serviço a serviço", ["integration", "architecture"]],
236
379
  ["secret rotation", ["secrets"]],
@@ -271,7 +414,7 @@ function taskMatchesKeyword(taskLower, keyword) {
271
414
  }
272
415
  const VAGUE_PATTERNS = [
273
416
  {
274
- pattern: /\b(torna|make).{0,40}\b(seguro|secure)\b/i,
417
+ pattern: /\b(torna|make).{0,40}\b(segur[ao]|secure)\b/i,
275
418
  reason: "Pedido excessivamente abrangente ('make secure' / 'tornar seguro')"
276
419
  },
277
420
  {
@@ -305,7 +448,7 @@ const VAGUE_PATTERNS = [
305
448
  * these, so the request is unsupported rather than decomposable.
306
449
  */
307
450
  const UNSUPPORTED_TECH_PATTERN = /\b(homomorphic|quantum[- ]?(resistant|safe)?|post[- ]?quantum|blockchain|smart contract|zero[- ]?knowledge|zk[- ]?(snark|stark|proof)s?|secure multiparty|federated learning)\b/i;
308
- function normalizeInput(raw) {
451
+ export function normalizeInput(raw) {
309
452
  const data = (typeof raw === "object" && raw !== null ? raw : {});
310
453
  const task = typeof data.task === "string" ? data.task : "";
311
454
  const taskTrimmed = task.trim();
@@ -417,7 +560,7 @@ function recordActivation(trace, concerns, scores, rejected, entry, targetConcer
417
560
  trace.push(entry);
418
561
  }
419
562
  }
420
- function activate(input) {
563
+ export function activate(input) {
421
564
  const trace = [];
422
565
  const rejected = [];
423
566
  const notes = [];
@@ -551,6 +694,45 @@ function activate(input) {
551
694
  reason: `Concern '${concern}' maps to AppSec Core slice family '${family}'.`
552
695
  });
553
696
  }
697
+ // 4b) Declared context activators (G-mp1a / D3, 2026-08-31): exposure and
698
+ // data_sensitivity stop being decorative — they activate concerns by DECLARED
699
+ // rule (each with its own trace source), because the reference selection
700
+ // semantics says an authenticated/public surface must be auditable and a
701
+ // personal/regulated data context must carry crypto+masking+validation.
702
+ const EXPOSURE_CONCERNS = {
703
+ internal: ["auth", "logging"],
704
+ authenticated: ["auth", "logging"],
705
+ public: ["auth", "logging", "api", "validation", "architecture"]
706
+ };
707
+ if (input.exposure && EXPOSURE_CONCERNS[input.exposure]) {
708
+ for (const concern of EXPOSURE_CONCERNS[input.exposure] ?? []) {
709
+ recordActivation(trace, concerns, concernScores, rejected, {
710
+ source: "exposure",
711
+ produced: concern,
712
+ trigger: input.exposure,
713
+ score: 0.9,
714
+ confidence: "deterministic",
715
+ reason: `exposure='${input.exposure}' activates ${concern} by declared rule (auditable exposed surface).`
716
+ }, concern, { capDuplicates: true });
717
+ }
718
+ }
719
+ const SENSITIVITY_CONCERNS = {
720
+ personal: ["encryption", "validation", "logging"],
721
+ regulated: ["encryption", "validation", "logging"],
722
+ secrets: ["secrets"]
723
+ };
724
+ if (input.data_sensitivity && SENSITIVITY_CONCERNS[input.data_sensitivity]) {
725
+ for (const concern of SENSITIVITY_CONCERNS[input.data_sensitivity] ?? []) {
726
+ recordActivation(trace, concerns, concernScores, rejected, {
727
+ source: "data_sensitivity",
728
+ produced: concern,
729
+ trigger: input.data_sensitivity,
730
+ score: 0.9,
731
+ confidence: "deterministic",
732
+ reason: `data_sensitivity='${input.data_sensitivity}' activates ${concern} by declared rule (ENC/masking/validation for personal or regulated data).`
733
+ }, concern, { capDuplicates: true });
734
+ }
735
+ }
554
736
  // 5) Risk level (informational trace entry, no concern activation).
555
737
  if (input.risk_level) {
556
738
  trace.push({
@@ -562,8 +744,39 @@ function activate(input) {
562
744
  reason: `Risk level ${input.risk_level} filters runtime v0 requirements.`
563
745
  });
564
746
  }
747
+ // P3 (2026-08-31): primary-concern families for the decomposition gate.
748
+ const primaryOfSignal = new Map();
749
+ for (const [term, mapped] of TASK_TERM_TO_CONCERNS) {
750
+ if (mapped.length > 0)
751
+ primaryOfSignal.set(term, mapped[0]);
752
+ }
753
+ for (const [phrase, mapped] of COMPOUND_TERM_TO_CONCERNS) {
754
+ if (mapped.length > 0)
755
+ primaryOfSignal.set(phrase, mapped[0]);
756
+ }
757
+ const primaryConcerns = new Set();
758
+ for (const entry of trace) {
759
+ if (entry.source === "risk_level" ||
760
+ entry.source === "exposure" ||
761
+ entry.source === "data_sensitivity" ||
762
+ entry.source === "scope_gate") {
763
+ continue; // contexto/informativos — não são superfícies
764
+ }
765
+ if (!concerns.has(entry.produced))
766
+ continue;
767
+ const rowPrimary = primaryOfSignal.get(entry.trigger);
768
+ if (rowPrimary === undefined || rowPrimary === entry.produced) {
769
+ primaryConcerns.add(entry.produced);
770
+ }
771
+ }
772
+ const decompositionFamilies = [
773
+ ...new Set([...primaryConcerns]
774
+ .map((concern) => CONCERN_TO_SLICE_FAMILY[concern])
775
+ .filter((family) => typeof family === "string"))
776
+ ].sort();
565
777
  return {
566
778
  concerns: [...concerns],
779
+ decompositionFamilies,
567
780
  sliceFamilies: [...sliceFamilyScores.keys()].sort((a, b) => (sliceFamilyScores.get(b) ?? 0) - (sliceFamilyScores.get(a) ?? 0) ||
568
781
  a.localeCompare(b)),
569
782
  trace,
@@ -617,21 +830,24 @@ function gateAfterActivation(args) {
617
830
  const { input, activation, estimatedRequirements } = args;
618
831
  const reasons = [];
619
832
  const suggestions = [];
620
- if (activation.sliceFamilies.length > 3) {
621
- reasons.push(`Pedido activa ${activation.sliceFamilies.length} slice families (${activation.sliceFamilies.join(", ")})máximo recomendado: 3.`);
833
+ // P3 do ciclo MP1 (2026-08-31): o gate conta SUPERFÍCIES (famílias dos concerns
834
+ // primários de cada sinal), não o total de famílias activadas concerns de
835
+ // suporte de um mesmo sinal (mtls→secrets, mensageria→logging) não pedem
836
+ // decomposição. GC-10 é o caso de referência: 1 integração legítima.
837
+ if (activation.decompositionFamilies.length > 3) {
838
+ reasons.push(`Pedido activa ${activation.decompositionFamilies.length} superfícies (famílias primárias: ${activation.decompositionFamilies.join(", ")}) — máximo recomendado: 3. Concerns de suporte do mesmo sinal não contam.`);
622
839
  suggestions.push("Reparte por slice family. Cada PR/PR-step deve ficar em 1–3 slices.");
623
840
  }
624
- // Hard requirement cap: above ~50 requirements the LLM context becomes
625
- // unfocused and asks should be decomposed. Slice-family count (max 3) is the
626
- // primary decomposition signal; this cap catches multi-concern asks that
627
- // sneak under the slice-family threshold.
628
- if (estimatedRequirements > 50) {
629
- reasons.push(`Pedido activaria ${estimatedRequirements} requisitos v0 — máximo permitido para codegen: 50.`);
630
- suggestions.push("Reduz o âmbito (risk_level mais baixo, concerns mais específicos, ou divide o endpoint).");
631
- }
632
- if (activation.concerns.length === 0 &&
633
- input.tokenCount >= 4 &&
634
- activation.trace.length === 0) {
841
+ // G-mp1a decision 2 (2026-08-31, D1): the former hard cap "max 50 activated
842
+ // requirements" is GONE a legitimate L2 task activates >50 by design (the
843
+ // cap 02 baseline is a real catalogue). The gate guards TASK scope (vague /
844
+ // multi-family asks above) and PAYLOAD (the detail diet + budgets), never a
845
+ // requirement count. estimatedRequirements stays as a debug figure only.
846
+ void estimatedRequirements;
847
+ // D1 (G-mp1a): with the requirement-count cap gone, the no-signal guard is the
848
+ // vagueness catch-all. The informational risk_level trace entry must not defeat
849
+ // it — only real signals (concerns) count.
850
+ if (activation.concerns.length === 0 && input.tokenCount >= 4) {
635
851
  return {
636
852
  status: "needs_clarification",
637
853
  reasons: [
@@ -685,7 +901,7 @@ function projectRelation(relation) {
685
901
  source: "runtime_v1"
686
902
  };
687
903
  }
688
- function categoriesForConcerns(concerns) {
904
+ export function categoriesForConcerns(concerns) {
689
905
  const ontology = getOntologyData();
690
906
  const categories = new Set();
691
907
  for (const concern of concerns) {
@@ -701,12 +917,20 @@ function categoriesForConcerns(concerns) {
701
917
  function resolveRuntimeV0(args) {
702
918
  const ontology = getOntologyData();
703
919
  const concernCategories = categoriesForConcerns(args.concerns);
704
- let filteredRequirements = ontology.requirements;
705
- if (args.riskLevel) {
706
- filteredRequirements = filteredRequirements.filter((requirement) => requirement.applicable_levels?.[args.riskLevel] === true);
920
+ let filteredRequirements;
921
+ if (args.selectedRequirements) {
922
+ // MP1 engine (G-mp1a O2): the selection operation already produced the set
923
+ // (baseline ∪ context ⊕ narrowing, all declared) — use it verbatim.
924
+ filteredRequirements = args.selectedRequirements;
707
925
  }
708
- if (concernCategories.size > 0) {
709
- filteredRequirements = filteredRequirements.filter((requirement) => concernCategories.has(requirement.category));
926
+ else {
927
+ filteredRequirements = ontology.requirements;
928
+ if (args.riskLevel) {
929
+ filteredRequirements = filteredRequirements.filter((requirement) => requirement.applicable_levels?.[args.riskLevel] === true);
930
+ }
931
+ if (concernCategories.size > 0) {
932
+ filteredRequirements = filteredRequirements.filter((requirement) => concernCategories.has(requirement.category));
933
+ }
710
934
  }
711
935
  const links = ontology.requirementControlLinks ?? [];
712
936
  const requirementIds = new Set(filteredRequirements.map((r) => r.requirement_id));
@@ -926,32 +1150,237 @@ function resolveOverlay(input) {
926
1150
  activatedFrameworks
927
1151
  };
928
1152
  }
929
- // ---------------------------------------------------------------------------
930
- // LLM instructions
931
- // ---------------------------------------------------------------------------
932
- function buildLlmInstructions(args) {
933
- const instructions = [];
934
- instructions.push("Generate code or review changes ONLY against the deterministic IDs provided in `citation_map`. Do NOT invent SbD-ToE requirement, control, slice, mechanism or obligation IDs.");
935
- instructions.push("For each non-trivial design decision, populate the `security_rationale_template.decisions[].cited_ids` with IDs from `citation_map`. If no ID applies, say so explicitly.");
936
- instructions.push("List concrete validations in `security_rationale_template.validations` (surface, rule, rejection behaviour). Do NOT claim conformity without naming the validation.");
937
- instructions.push("List expected evidence in `security_rationale_template.expected_evidence` (test paths, log shapes, SBOM, attestation, scan reports). Code on its own is NOT evidence of compliance.");
938
- if (args.hasOverlay) {
939
- instructions.push("Regulatory obligations are an EXTERNAL cross-check. Cite obligation IDs in security_rationale only when the change directly addresses them. Do NOT declare GDPR/DORA/CRA/NIS2 compliance.");
940
- }
941
- instructions.push("If the requested task does not match the activated scope, REPLY with `status: needs_clarification` and request specifics — do not fabricate IDs.");
942
- if (args.mode === "review") {
943
- instructions.push("Review mode: enumerate findings per changed_file, mapped to the activated_scope. Each finding must reference at least one citation_map ID or say 'no normative ID covers this'.");
944
- }
945
- if (args.mode === "test-plan") {
946
- instructions.push("Test-plan mode: produce a checklist of tests grouped by validated_id, with input/expectation, and reference evidence_patterns when available.");
1153
+ /**
1154
+ * Ordered instruction slots for a mode. The emission order of
1155
+ * {@link buildLlmInstructions} is EXACTLY this list filtered by active
1156
+ * conditions — the classic (pre-s3) output is byte-identical by construction.
1157
+ */
1158
+ export function instructionSlotsForMode(mode) {
1159
+ const slots = [
1160
+ {
1161
+ when: "always",
1162
+ text: "Generate code or review changes ONLY against the deterministic IDs provided in `citation_map`. Do NOT invent SbD-ToE requirement, control, slice, mechanism or obligation IDs."
1163
+ },
1164
+ {
1165
+ when: "always",
1166
+ text: "For each non-trivial design decision, populate the `security_rationale_template.decisions[].cited_ids` with IDs from `citation_map`. If no ID applies, say so explicitly."
1167
+ },
1168
+ {
1169
+ when: "always",
1170
+ text: "List concrete validations in `security_rationale_template.validations` (surface, rule, rejection behaviour). Do NOT claim conformity without naming the validation."
1171
+ },
1172
+ {
1173
+ when: "always",
1174
+ text: "List expected evidence in `security_rationale_template.expected_evidence` (test paths, log shapes, SBOM, attestation, scan reports). Code on its own is NOT evidence of compliance."
1175
+ },
1176
+ {
1177
+ when: "regulatory_overlay",
1178
+ text: "Regulatory obligations are an EXTERNAL cross-check. Cite obligation IDs in security_rationale only when the change directly addresses them. Do NOT declare GDPR/DORA/CRA/NIS2 compliance."
1179
+ },
1180
+ {
1181
+ when: "always",
1182
+ text: "If the requested task does not match the activated scope, REPLY with `status: needs_clarification` and request specifics — do not fabricate IDs."
1183
+ }
1184
+ ];
1185
+ if (mode === "review") {
1186
+ slots.push({
1187
+ when: "always",
1188
+ text: "Review mode: enumerate findings per changed_file, mapped to the activated_scope. Each finding must reference at least one citation_map ID or say 'no normative ID covers this'."
1189
+ });
947
1190
  }
948
- if (args.riskLevel) {
949
- instructions.push(`Risk level ${args.riskLevel} is the active filter — do not propose controls applicable only at a higher level unless explicitly justified.`);
1191
+ if (mode === "test-plan") {
1192
+ slots.push({
1193
+ when: "always",
1194
+ text: "Test-plan mode: produce a checklist of tests grouped by validated_id, with input/expectation, and reference evidence_patterns when available."
1195
+ });
950
1196
  }
951
- if (args.citedIds.length === 0) {
952
- instructions.push("Citation_map is empty. This is a strong signal the activated scope did not yield deterministic anchors — request clarification before generating code.");
1197
+ for (const level of ["L1", "L2", "L3"]) {
1198
+ slots.push({
1199
+ when: `risk_level:${level}`,
1200
+ text: `Risk level ${level} is the active filter — do not propose controls applicable only at a higher level unless explicitly justified.`
1201
+ });
953
1202
  }
954
- return instructions;
1203
+ slots.push({
1204
+ when: "citation_map_empty",
1205
+ text: "Citation_map is empty. This is a strong signal the activated scope did not yield deterministic anchors — request clarification before generating code."
1206
+ });
1207
+ return slots;
1208
+ }
1209
+ /** Conditional slots active for a call (deterministic, from resolved inputs). */
1210
+ function activeInstructionConditions(args) {
1211
+ const active = [];
1212
+ if (args.hasOverlay)
1213
+ active.push("regulatory_overlay");
1214
+ if (args.riskLevel)
1215
+ active.push(`risk_level:${args.riskLevel}`);
1216
+ if (args.citationMapEmpty)
1217
+ active.push("citation_map_empty");
1218
+ return active;
1219
+ }
1220
+ function buildLlmInstructions(args) {
1221
+ const active = new Set(activeInstructionConditions({
1222
+ hasOverlay: args.hasOverlay,
1223
+ riskLevel: args.riskLevel,
1224
+ citationMapEmpty: args.citedIds.length === 0
1225
+ }));
1226
+ return instructionSlotsForMode(args.mode)
1227
+ .filter((slot) => slot.when === "always" || active.has(slot.when))
1228
+ .map((slot) => slot.text);
1229
+ }
1230
+ /** Constant part of the security_rationale_template (everything except `task`). */
1231
+ const SECURITY_RATIONALE_TEMPLATE_SKELETON = {
1232
+ decisions: [
1233
+ {
1234
+ decision: "<fill: what design choice was made>",
1235
+ rationale: "<fill: why, citing IDs from citation_map>",
1236
+ cited_ids: ["<requirement_id|control_id|slice_id|obligation_id>"]
1237
+ }
1238
+ ],
1239
+ validations: [
1240
+ {
1241
+ surface: "<fill: code path being validated>",
1242
+ rule: "<fill: validation rule>",
1243
+ rejection_behaviour: "<fill: how invalid input is rejected>"
1244
+ }
1245
+ ],
1246
+ expected_evidence: [
1247
+ {
1248
+ artefact: "<fill: test, log, doc, sbom, scan, attestation, ...>",
1249
+ location: "<fill: where to find it>",
1250
+ verifies: "<fill: which control/requirement id>"
1251
+ }
1252
+ ],
1253
+ residual_risk: "<fill: anything NOT addressed by this change>"
1254
+ };
1255
+ function buildSecurityRationaleTemplate(task) {
1256
+ return { task, ...SECURITY_RATIONALE_TEMPLATE_SKELETON };
1257
+ }
1258
+ // ---------------------------------------------------------------------------
1259
+ // MCP resource: sbd://toe/codegen-instructions/{mode} (v2 token diet, s3)
1260
+ // ---------------------------------------------------------------------------
1261
+ export const CODEGEN_INSTRUCTION_MODES = [
1262
+ "codegen",
1263
+ "review",
1264
+ "test-plan"
1265
+ ];
1266
+ export const CODEGEN_INSTRUCTIONS_RESOURCE_URI_PREFIX = "sbd://toe/codegen-instructions/";
1267
+ export function codegenInstructionsResourceUri(mode) {
1268
+ return `${CODEGEN_INSTRUCTIONS_RESOURCE_URI_PREFIX}${mode}`;
1269
+ }
1270
+ /**
1271
+ * Full legend of the dieted (`standard`/`minimal`) encoding, published in the
1272
+ * codegen-instructions resource. Every rule here is a lossless, deterministic
1273
+ * derivation over the SAME payload (or an executable reference) — nothing is
1274
+ * silently dropped (EPIC invariant 2) and no data changes, only serialization
1275
+ * (EPIC invariant 4).
1276
+ */
1277
+ const DETAIL_ENCODING_LEGEND = {
1278
+ note: "How to read a detail=standard/minimal payload of prepare_sbd_toe_codegen_context. " +
1279
+ "Every rule below is a deterministic re-encoding of the same published data: " +
1280
+ "nothing is silently dropped, and detail=full always returns the classic inline payload.",
1281
+ sources: {
1282
+ note: "Per-item `source` fields are elided. Every list below is source-homogeneous " +
1283
+ "(no exceptions): apply the listed source to each of its items. " +
1284
+ "g2_context.relations applies only when relations come inline " +
1285
+ "(include_relations=true); otherwise g2_context.relations_ref is a derived " +
1286
+ "reference to trace_sbd_toe_graph calls, not a source list.",
1287
+ map: PROVENANCE_SOURCES
1288
+ },
1289
+ citations: "citations.<source>.source_data is an ordered run-length map file -> count. " +
1290
+ "The citable ids are NOT repeated: citations.<source>.ids_from is aligned 1:1 " +
1291
+ "with the source_data files, and names the payload path whose ids (in payload " +
1292
+ "order) form that file's run. Paths of the form " +
1293
+ "keys(g2_context.<list>[slice]) iterate the slice groups in order, then the " +
1294
+ "entity-id keys in order. If a file ever has no path mapping, the group " +
1295
+ "carries explicit `ids` instead (lossless fallback).",
1296
+ activated_scope_requirements: "requirement `category` is elided because it equals the requirement_id " +
1297
+ "category segment — the one immediately before the number (AUT-003→AUT, " +
1298
+ "REQ-AGN-001→AGN; consumer contract v1.10 §1.18). Verbatim bundle invariant; " +
1299
+ "the field survives inline on any future mismatch. `description` is the verbatim published field from " +
1300
+ "data/publish/runtime/requirements.json — never paraphrased.",
1301
+ activated_scope_controls: "controls with confidence='direct' carry the verbatim published `description` " +
1302
+ "from data/publish/runtime/controls.json.",
1303
+ g2_entities: "g2_context.control_objectives/mechanisms/practices/artifacts are grouped as " +
1304
+ "{slice_id: {entity_id: name|null}}. entity_type is the list the map lives in " +
1305
+ "(ControlObjective/Mechanism/Practice/Artifact), slice_id is the group key, " +
1306
+ "slice_family is activated_scope.slices[].objective_family for that slice_id, " +
1307
+ "and a null name means the entity is unnamed in the published rastreabilidade " +
1308
+ "(the full projection omits `name` for it).",
1309
+ evidence_patterns: "g2_context.evidence_patterns is the deterministic prefix (relevance desc, " +
1310
+ "then id asc; the tool-computed relevance_score is elided — the order carries " +
1311
+ "the ranking) of the classic detail=full list, capped per " +
1312
+ "completeness_report.evidence_pattern_cap. completeness_report reports " +
1313
+ "total/returned/capped and, when anything was cut, evidence_patterns_rest " +
1314
+ "says how to retrieve the rest (same input, detail='full'; with debug=true " +
1315
+ "the ids beyond the classic cap are listed in debug.rejected_candidates).",
1316
+ manual_grounding_minimal: "At detail=minimal, manual_grounding is the aggregated-provenance form " +
1317
+ "(s3b): total_entries, the manual_commit_sha shared by every group " +
1318
+ "(hoisted; a group keeps its own sha inline only if hoisting was not " +
1319
+ "possible), and the (rastreabilidade_role, manual_chapter, manual_file) " +
1320
+ "groups with the exact per-group `entries` COUNT instead of the " +
1321
+ "v1_entity_ids list (counts sum to total_entries — never silent). The " +
1322
+ "grounding id set is already in the same payload (g2_context entity-map " +
1323
+ "keys); manual_grounding.groups_ref is the executable reference (same " +
1324
+ "input, detail='standard') for the per-group id lists — groups align " +
1325
+ "1:1, same order. detail=standard keeps the full grouping inline.",
1326
+ activation_trace: "activation_trace is elided at detail=standard/minimal; " +
1327
+ "activation_trace_ref.entries keeps the exact count. Re-call with debug=true " +
1328
+ "to include the full trace (it is always inline at detail=full).",
1329
+ relations_ref: "Inline g2_context.relations are elided at detail=standard/minimal (re-call " +
1330
+ "with include_relations=true to restore them). Recover the elided graph edges " +
1331
+ "by executing trace_sbd_toe_graph with each {lens, anchor} pair listed " +
1332
+ "(anchors are activated slice/entity ids from the same payload); the " +
1333
+ "belongsToSlice edges counted as coverage.implicit_in_entities are already " +
1334
+ "encoded by the slice grouping key of every g2_context entity; any relation " +
1335
+ "covered by neither stays inline in residual_relations (never silent).",
1336
+ ultrathin: "detail=ultrathin (s3c) applies every rule above PLUS: (1) requirements " +
1337
+ "{requirement_id, name, type} and controls {control_id, name, domain, " +
1338
+ "control_type, confidence} keep the COMPLETE activated set (same ids, " +
1339
+ "same order, name always present) but elide the published `description` " +
1340
+ "— activated_scope.descriptions_ref is the executable reference (same " +
1341
+ "input, detail='minimal') for the verbatim descriptions; (2) " +
1342
+ "g2_context.evidence_patterns is empty (cap 0) — completeness_report " +
1343
+ "keeps total/returned=0/capped=total and evidence_patterns_rest points " +
1344
+ "to detail='minimal' (cheapest level returning patterns inline; " +
1345
+ "'standard' returns 10, 'full' the classic 25); (3) manual_grounding is " +
1346
+ "{total_entries, manual_commit_sha, groups_ref} — the group list is " +
1347
+ "elided (detail='standard' returns the full 1:1 grouping); (4) " +
1348
+ "v1_consistency_mismatches/v1_manifest_warnings text arrays are replaced " +
1349
+ "by exact *_count fields (+ v1_diagnostics_ref when any count > 0, " +
1350
+ "detail='minimal' returns the texts). The citable id set and the " +
1351
+ "citations/ids_from encoding are IDENTICAL to the other dieted levels."
1352
+ };
1353
+ /**
1354
+ * Content of the `sbd://toe/codegen-instructions/{mode}` MCP resource — the
1355
+ * static-per-mode boilerplate that detail=standard/minimal payloads reference
1356
+ * instead of carrying inline. Reconstructing the inline `full` content from
1357
+ * this resource is byte-exact (tested):
1358
+ * - llm_codegen_instructions = slots filtered by `when` ("always" +
1359
+ * codegen_instructions_ref.active_conditions), in order;
1360
+ * - security_rationale_template = template with `task` set to the trimmed
1361
+ * task string (input_echo.task.trim()).
1362
+ */
1363
+ export function buildCodegenInstructionsResourceContent(mode) {
1364
+ return {
1365
+ resource: codegenInstructionsResourceUri(mode),
1366
+ mode,
1367
+ note: "Static per-mode boilerplate for prepare_sbd_toe_codegen_context at " +
1368
+ "detail=standard/minimal (kept inline at detail=full). Also carries the " +
1369
+ "detail_encoding legend for the dieted payload.",
1370
+ llm_codegen_instructions: {
1371
+ assembly: "Include each slot whose `when` is 'always' or appears in this call's " +
1372
+ "codegen_instructions_ref.active_conditions, in the listed order — the " +
1373
+ "result is byte-identical to the detail=full inline llm_codegen_instructions.",
1374
+ slots: instructionSlotsForMode(mode)
1375
+ },
1376
+ security_rationale_template: {
1377
+ assembly: "Set `task` to the trimmed task string (input_echo.task.trim()); every " +
1378
+ "other field is verbatim — the result is byte-identical to the " +
1379
+ "detail=full inline security_rationale_template.",
1380
+ template: { task: null, ...SECURITY_RATIONALE_TEMPLATE_SKELETON }
1381
+ },
1382
+ detail_encoding: DETAIL_ENCODING_LEGEND
1383
+ };
955
1384
  }
956
1385
  // ---------------------------------------------------------------------------
957
1386
  // Main entry point
@@ -976,10 +1405,691 @@ function blocked(input, raw, status, reasons, suggestions, partial, debug) {
976
1405
  }
977
1406
  return result;
978
1407
  }
1408
+ const DETAIL_LEVELS = new Set([
1409
+ "ultrathin",
1410
+ "minimal",
1411
+ "standard",
1412
+ "full"
1413
+ ]);
1414
+ /**
1415
+ * Validate the `detail` input (v2 token diet, s1). Invalid values fail fast
1416
+ * with a JSON-RPC -32602 (same pattern as trace-graph's lens validation);
1417
+ * omission defaults to `full` — the classic, byte-identical payload.
1418
+ */
1419
+ function parseDetail(raw) {
1420
+ const value = typeof raw === "object" && raw !== null
1421
+ ? raw.detail
1422
+ : undefined;
1423
+ if (value === undefined)
1424
+ return "full";
1425
+ if (typeof value === "string" && DETAIL_LEVELS.has(value)) {
1426
+ return value;
1427
+ }
1428
+ throw Object.assign(new Error(`Invalid "detail": ${JSON.stringify(value)}. Use one of: ultrathin, minimal, standard, full.`), {
1429
+ rpcError: {
1430
+ code: -32602,
1431
+ message: 'Invalid "detail". Use one of: ultrathin, minimal, standard, full.'
1432
+ }
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Validate the `include_relations` input (v2 token diet, s2). Only booleans
1437
+ * (or omission = false) are accepted — same fail-fast pattern as parseDetail.
1438
+ */
1439
+ function parseIncludeRelations(raw) {
1440
+ const value = typeof raw === "object" && raw !== null
1441
+ ? raw.include_relations
1442
+ : undefined;
1443
+ if (value === undefined)
1444
+ return false;
1445
+ if (typeof value === "boolean")
1446
+ return value;
1447
+ throw Object.assign(new Error(`Invalid "include_relations": ${JSON.stringify(value)}. Use a boolean.`), {
1448
+ rpcError: {
1449
+ code: -32602,
1450
+ message: 'Invalid "include_relations". Use a boolean.'
1451
+ }
1452
+ });
1453
+ }
1454
+ // Relation predicates published in data/publish/runtime/v1/relations.jsonl —
1455
+ // the same three the RDF projection exposes to trace_sbd_toe_graph lenses.
1456
+ const PRED_BELONGS_TO_SLICE = "belongsToSlice";
1457
+ const PRED_IMPLEMENTED_BY_MECHANISM = "objective_implemented_by_mechanism";
1458
+ const PRED_REALIZED_BY_PRACTICE = "objective_realized_by_practice";
1459
+ // s3: slimmed — the full explanation lives in the codegen-instructions MCP
1460
+ // resource (detail_encoding.relations_ref). Kept URI-free on purpose: the
1461
+ // no-leak gate scans relations_ref for any scheme://.
1462
+ const RELATIONS_REF_NOTE = "Inline g2_context.relations elided; execute each listed trace_sbd_toe_graph " +
1463
+ "{lens, anchor} call to recover them, or re-call with include_relations=true. " +
1464
+ "Encoding details: MCP resource codegen-instructions, detail_encoding.relations_ref.";
1465
+ /**
1466
+ * v2 token diet, s2 — build the `relations_ref` for `detail: "standard" |
1467
+ * "minimal"`. See the {@link RelationsRefLensCall} doc block for the full
1468
+ * relation-kind → lens mapping. Coverage is decided per relation and counted
1469
+ * (never-silent): every inline relation is either recoverable by executing
1470
+ * one of the referenced curated lenses, byte-redundant with an entity's
1471
+ * `slice_id` in this same payload, or kept inline in `residual_relations`.
1472
+ * Deterministic: lens order follows activated_scope.slices order, then sorted
1473
+ * fallback anchors.
1474
+ */
1475
+ function buildRelationsRef(result) {
1476
+ const relations = result.g2_context.relations;
1477
+ const activatedSliceIds = result.activated_scope.slices.map((slice) => slice.slice_id);
1478
+ const activatedSliceIdSet = new Set(activatedSliceIds);
1479
+ const entitySliceById = new Map();
1480
+ const entityTypeById = new Map();
1481
+ for (const list of [
1482
+ result.g2_context.control_objectives,
1483
+ result.g2_context.mechanisms,
1484
+ result.g2_context.practices,
1485
+ result.g2_context.artifacts
1486
+ ]) {
1487
+ for (const entity of list) {
1488
+ entitySliceById.set(entity.entity_id, entity.slice_id);
1489
+ entityTypeById.set(entity.entity_id, entity.entity_type);
1490
+ }
1491
+ }
1492
+ // Objective → activated slice via an explicit belongsToSlice edge (the
1493
+ // pattern slice_implementation anchors on), and objectives that have at
1494
+ // least one mechanism/practice edge (required by that lens's UNION).
1495
+ const sliceEdgeBySubject = new Map();
1496
+ const subjectsWithTargets = new Set();
1497
+ for (const relation of relations) {
1498
+ if (relation.predicate === PRED_BELONGS_TO_SLICE &&
1499
+ activatedSliceIdSet.has(relation.object_id)) {
1500
+ if (!sliceEdgeBySubject.has(relation.subject_id)) {
1501
+ sliceEdgeBySubject.set(relation.subject_id, relation.object_id);
1502
+ }
1503
+ }
1504
+ else if (relation.predicate === PRED_IMPLEMENTED_BY_MECHANISM ||
1505
+ relation.predicate === PRED_REALIZED_BY_PRACTICE) {
1506
+ subjectsWithTargets.add(relation.subject_id);
1507
+ }
1508
+ }
1509
+ const sliceImplementationAnchors = new Set();
1510
+ const objectiveRealizationAnchors = new Set();
1511
+ const mechanismProvenanceAnchors = new Set();
1512
+ let viaLenses = 0;
1513
+ let implicitInEntities = 0;
1514
+ const residual = [];
1515
+ for (const relation of relations) {
1516
+ if (relation.predicate === PRED_BELONGS_TO_SLICE) {
1517
+ if (activatedSliceIdSet.has(relation.object_id) &&
1518
+ subjectsWithTargets.has(relation.subject_id)) {
1519
+ // slice_implementation(anchor=slice) rows carry this edge.
1520
+ sliceImplementationAnchors.add(relation.object_id);
1521
+ viaLenses += 1;
1522
+ }
1523
+ else if (entitySliceById.get(relation.subject_id) === relation.object_id) {
1524
+ // Redundant with the entity's own slice_id in g2_context.
1525
+ implicitInEntities += 1;
1526
+ }
1527
+ else {
1528
+ const { source: _source, ...rest } = relation;
1529
+ residual.push(rest);
1530
+ }
1531
+ continue;
1532
+ }
1533
+ if (relation.predicate === PRED_IMPLEMENTED_BY_MECHANISM ||
1534
+ relation.predicate === PRED_REALIZED_BY_PRACTICE) {
1535
+ const sliceAnchor = sliceEdgeBySubject.get(relation.subject_id);
1536
+ if (sliceAnchor !== undefined) {
1537
+ sliceImplementationAnchors.add(sliceAnchor);
1538
+ viaLenses += 1;
1539
+ }
1540
+ else if (entitySliceById.has(relation.subject_id)) {
1541
+ // Activated objective without a belongsToSlice edge (data gap).
1542
+ objectiveRealizationAnchors.add(relation.subject_id);
1543
+ viaLenses += 1;
1544
+ }
1545
+ else if ((relation.predicate === PRED_IMPLEMENTED_BY_MECHANISM &&
1546
+ entityTypeById.get(relation.object_id) === "Mechanism") ||
1547
+ (relation.predicate === PRED_REALIZED_BY_PRACTICE &&
1548
+ entityTypeById.get(relation.object_id) === "Practice")) {
1549
+ // Only the target is activated (cross-slice edge); the predicate is
1550
+ // recoverable from the target's entity_type in this payload.
1551
+ mechanismProvenanceAnchors.add(relation.object_id);
1552
+ viaLenses += 1;
1553
+ }
1554
+ else {
1555
+ const { source: _source, ...rest } = relation;
1556
+ residual.push(rest);
1557
+ }
1558
+ continue;
1559
+ }
1560
+ // Unknown predicate — never silently dropped.
1561
+ const { source: _source, ...rest } = relation;
1562
+ residual.push(rest);
1563
+ }
1564
+ const lenses = [
1565
+ ...activatedSliceIds
1566
+ .filter((sliceId) => sliceImplementationAnchors.has(sliceId))
1567
+ .map((anchor) => ({ lens: "slice_implementation", anchor })),
1568
+ ...[...objectiveRealizationAnchors]
1569
+ .sort()
1570
+ .map((anchor) => ({ lens: "objective_realization", anchor })),
1571
+ ...[...mechanismProvenanceAnchors]
1572
+ .sort()
1573
+ .map((anchor) => ({ lens: "mechanism_provenance", anchor }))
1574
+ ];
1575
+ const relationsRef = {
1576
+ tool: "trace_sbd_toe_graph",
1577
+ lenses,
1578
+ total_relations: relations.length,
1579
+ coverage: {
1580
+ via_lenses: viaLenses,
1581
+ implicit_in_entities: implicitInEntities,
1582
+ residual_inline: residual.length
1583
+ },
1584
+ note: RELATIONS_REF_NOTE
1585
+ };
1586
+ if (residual.length > 0)
1587
+ relationsRef.residual_relations = residual;
1588
+ return relationsRef;
1589
+ }
1590
+ function stripSource(items) {
1591
+ return items.map(({ source: _source, ...rest }) => rest);
1592
+ }
1593
+ /**
1594
+ * Static map published-file → payload path whose ids, in payload order, are
1595
+ * exactly the citation_map run for that file (the citation_map is BUILT by
1596
+ * iterating those very lists, in this order — see the core's citation block).
1597
+ * Paths use the mini-syntax documented in the resource's
1598
+ * `detail_encoding.citations` legend. No file outside this table is expected;
1599
+ * if one ever appears, the group falls back to explicit `ids` (lossless).
1600
+ */
1601
+ const CITATION_FILE_TO_PAYLOAD_PATH = {
1602
+ "data/publish/runtime/requirements.json": "activated_scope.requirements[].requirement_id",
1603
+ "data/publish/runtime/controls.json": "activated_scope.controls[].control_id",
1604
+ "data/publish/runtime/v1/slices.json": "activated_scope.slices[].slice_id",
1605
+ "data/publish/runtime/v1/control_objectives.json": "keys(g2_context.control_objectives[slice])",
1606
+ "data/publish/runtime/v1/mechanisms.json": "keys(g2_context.mechanisms[slice])",
1607
+ "data/publish/runtime/v1/practices.json": "keys(g2_context.practices[slice])",
1608
+ "data/publish/runtime/v1/artifacts.json": "keys(g2_context.artifacts[slice])",
1609
+ "data/publish/overlay/external_frameworks.json": "regulatory_overlay.frameworks[].framework_id",
1610
+ "data/publish/overlay/external_obligations.json": "activated_scope.regulatory_obligations[].obligation_id"
1611
+ };
1612
+ /**
1613
+ * Invert the classic `citation_map` (id → {source, source_data}) into
1614
+ * source-grouped `citations` (see {@link CitationsGroup}). Pure re-encoding:
1615
+ * the exact per-id source and source_data are reconstructible from the
1616
+ * ordered run-length `source_data` map — nothing is dropped. s3: ids already
1617
+ * present verbatim in a payload section are referenced via `ids_from` instead
1618
+ * of repeated; a group keeps explicit `ids` only if one of its files has no
1619
+ * payload-path mapping (never expected for the published bundle).
1620
+ */
1621
+ function invertCitationMap(citationMap) {
1622
+ const bySource = new Map();
1623
+ for (const [id, entry] of Object.entries(citationMap)) {
1624
+ let files = bySource.get(entry.source);
1625
+ if (!files) {
1626
+ files = new Map();
1627
+ bySource.set(entry.source, files);
1628
+ }
1629
+ let ids = files.get(entry.source_data);
1630
+ if (!ids) {
1631
+ ids = [];
1632
+ files.set(entry.source_data, ids);
1633
+ }
1634
+ ids.push(id);
1635
+ }
1636
+ const citations = {};
1637
+ for (const [source, files] of bySource) {
1638
+ const source_data = {};
1639
+ const ids = [];
1640
+ const idsFrom = [];
1641
+ let allFilesMapped = true;
1642
+ for (const [file, fileIds] of files) {
1643
+ source_data[file] = fileIds.length;
1644
+ ids.push(...fileIds);
1645
+ const path = CITATION_FILE_TO_PAYLOAD_PATH[file];
1646
+ if (path === undefined)
1647
+ allFilesMapped = false;
1648
+ else
1649
+ idsFrom.push(path);
1650
+ }
1651
+ citations[source] = allFilesMapped
1652
+ ? { source_data, ids_from: idsFrom }
1653
+ : { source_data, ids };
1654
+ }
1655
+ return citations;
1656
+ }
1657
+ /**
1658
+ * Group the flat `manual_grounding` entries by the tuple that repeats
1659
+ * verbatim: (rastreabilidade_role, manual_chapter, manual_file,
1660
+ * manual_commit_sha). Names are elided ONLY when recoverable from the
1661
+ * `g2_context` entity lists in the same payload (they come from the same
1662
+ * rastreabilidade source); any non-recoverable name is kept explicitly in
1663
+ * `v1_entity_names`, so no information is lost.
1664
+ */
1665
+ function groupManualGrounding(result) {
1666
+ const g2Names = new Map();
1667
+ for (const list of [
1668
+ result.g2_context.control_objectives,
1669
+ result.g2_context.mechanisms,
1670
+ result.g2_context.practices,
1671
+ result.g2_context.artifacts
1672
+ ]) {
1673
+ for (const entity of list)
1674
+ g2Names.set(entity.entity_id, entity.name);
1675
+ }
1676
+ // Object sentinel: serializes unlike any string/null value, so an absent
1677
+ // field can never collide with a real published value in the group key.
1678
+ const ABSENT = { absent: true };
1679
+ const groups = new Map();
1680
+ const ungrouped = [];
1681
+ for (const entry of result.manual_grounding) {
1682
+ if (!entry.v1_entity_id) {
1683
+ // Lossless guard — the loader keys entries by v1_entity_id, so this is
1684
+ // not expected; if it ever happens the entry survives verbatim.
1685
+ const { source: _source, ...rest } = entry;
1686
+ ungrouped.push(rest);
1687
+ continue;
1688
+ }
1689
+ const hasChapter = "manual_chapter" in entry;
1690
+ const hasFile = "manual_file" in entry;
1691
+ const hasSha = entry.manual_commit_sha !== undefined;
1692
+ const key = JSON.stringify([
1693
+ entry.rastreabilidade_role,
1694
+ hasChapter ? entry.manual_chapter ?? null : ABSENT,
1695
+ hasFile ? entry.manual_file ?? null : ABSENT,
1696
+ hasSha ? entry.manual_commit_sha : ABSENT
1697
+ ]);
1698
+ let group = groups.get(key);
1699
+ if (!group) {
1700
+ group = {
1701
+ rastreabilidade_role: entry.rastreabilidade_role,
1702
+ ...(hasChapter ? { manual_chapter: entry.manual_chapter ?? null } : {}),
1703
+ ...(hasFile ? { manual_file: entry.manual_file ?? null } : {}),
1704
+ ...(hasSha ? { manual_commit_sha: entry.manual_commit_sha } : {}),
1705
+ v1_entity_ids: []
1706
+ };
1707
+ groups.set(key, group);
1708
+ }
1709
+ group.v1_entity_ids.push(entry.v1_entity_id);
1710
+ if (entry.v1_entity_name &&
1711
+ g2Names.get(entry.v1_entity_id) !== entry.v1_entity_name) {
1712
+ (group.v1_entity_names ??= {})[entry.v1_entity_id] = entry.v1_entity_name;
1713
+ }
1714
+ }
1715
+ const grouped = {
1716
+ total_entries: result.manual_grounding.length,
1717
+ groups: [...groups.values()]
1718
+ };
1719
+ if (ungrouped.length > 0)
1720
+ grouped.ungrouped = ungrouped;
1721
+ return grouped;
1722
+ }
1723
+ // s3b: kept URI-free on purpose (no-leak discipline, same as RELATIONS_REF_NOTE).
1724
+ const GROUNDING_GROUPS_REF_NOTE = "Per-group v1_entity_ids elided at detail=minimal (each group carries its " +
1725
+ "exact `entries` count). The grounding id set is already in this payload — " +
1726
+ "every grounding id is an entity-id key of the g2_context maps. Re-call " +
1727
+ "with the same input at detail='standard' for the per-group id lists " +
1728
+ "(groups align 1:1, same order).";
1729
+ /**
1730
+ * v2 token diet, s3b (revised ADENDA 2026-07-05) — minimal-form
1731
+ * `manual_grounding`, derived from the detail="standard" grouping (so the 1:1
1732
+ * group alignment holds by construction). Serialization-only cut, never
1733
+ * silent:
1734
+ * - per-group `v1_entity_ids` → exact `entries` count (Σ == total_entries);
1735
+ * - `manual_commit_sha` hoisted to the top level iff EVERY group carries
1736
+ * the same sha (expected always: one published manual commit); otherwise
1737
+ * each group keeps its own sha inline (lossless guard);
1738
+ * - `v1_entity_names` (never expected) and `ungrouped` (never expected)
1739
+ * survive verbatim — no name or entry can be lost;
1740
+ * - `groups_ref` is the executable reference to the full grouping.
1741
+ * Invariant-3 note: grounding ids never feed `citations`/`ids_from`, and the
1742
+ * id set stays reconstructible from this same payload's g2_context entity
1743
+ * maps without any extra call.
1744
+ */
1745
+ function buildMinimalGrounding(grouped) {
1746
+ const shas = grouped.groups.map((group) => group.manual_commit_sha);
1747
+ const hoistedSha = grouped.groups.length > 0 &&
1748
+ shas[0] !== undefined &&
1749
+ shas.every((sha) => sha === shas[0])
1750
+ ? shas[0]
1751
+ : undefined;
1752
+ const minimal = {
1753
+ total_entries: grouped.total_entries,
1754
+ ...(hoistedSha !== undefined ? { manual_commit_sha: hoistedSha } : {}),
1755
+ groups: grouped.groups.map((group) => ({
1756
+ rastreabilidade_role: group.rastreabilidade_role,
1757
+ ...("manual_chapter" in group ? { manual_chapter: group.manual_chapter } : {}),
1758
+ ...("manual_file" in group ? { manual_file: group.manual_file } : {}),
1759
+ ...(hoistedSha === undefined && group.manual_commit_sha !== undefined
1760
+ ? { manual_commit_sha: group.manual_commit_sha }
1761
+ : {}),
1762
+ entries: group.v1_entity_ids.length,
1763
+ ...(group.v1_entity_names ? { v1_entity_names: group.v1_entity_names } : {})
1764
+ })),
1765
+ groups_ref: {
1766
+ tool: "prepare_sbd_toe_codegen_context",
1767
+ with: { detail: "standard" },
1768
+ note: GROUNDING_GROUPS_REF_NOTE
1769
+ }
1770
+ };
1771
+ if (grouped.ungrouped)
1772
+ minimal.ungrouped = grouped.ungrouped;
1773
+ return minimal;
1774
+ }
1775
+ // s3c notes — all NEW constants (the s1–s4 note texts are byte-frozen by the
1776
+ // standard/minimal golden snapshots and are never edited). Kept URI-free
1777
+ // (no-leak discipline, same as RELATIONS_REF_NOTE).
1778
+ const GROUNDING_GROUPS_REF_NOTE_ULTRATHIN = "Grounding group list elided at detail=ultrathin (total_entries is the " +
1779
+ "exact flat entry count; manual_commit_sha is the shared published manual " +
1780
+ "commit). The grounding id set is already in this payload — every " +
1781
+ "grounding id is an entity-id key of the g2_context maps. Re-call with the " +
1782
+ "same input at detail='standard' for the full (role, chapter, file) groups " +
1783
+ "with per-group v1_entity_ids; detail='minimal' returns the groups with " +
1784
+ "per-group counts.";
1785
+ const DESCRIPTIONS_REF_NOTE = "Published `description` fields (the 'how') elided at detail=ultrathin — " +
1786
+ "the requirement/control lists themselves are COMPLETE (same ids, same " +
1787
+ "order, name always present). Re-call with the same input at " +
1788
+ "detail='minimal' for the same complete scope WITH the verbatim published " +
1789
+ "descriptions (requirements + direct controls).";
1790
+ const EVIDENCE_PATTERNS_REST_NOTE_ULTRATHIN = "No evidence pattern goes inline at detail=ultrathin (returned=0; " +
1791
+ "capped=total). Re-call with the same input at detail='minimal' for the " +
1792
+ "deterministic top-5 (cheapest level that returns patterns inline); " +
1793
+ "detail='standard' returns the top-10 and detail='full' the classic " +
1794
+ "top-25 (each list is a deterministic prefix of the next).";
1795
+ const ACTIVATION_TRACE_REF_NOTE_ULTRATHIN = "activation_trace elided at detail=ultrathin — re-call with debug=true to " +
1796
+ "include it (always inline at detail=full).";
1797
+ const V1_DIAGNOSTICS_REF_NOTE = "v1_consistency_mismatches/v1_manifest_warnings texts elided at " +
1798
+ "detail=ultrathin (exact counts inline). Re-call with the same input at " +
1799
+ "detail='minimal' for the full text arrays in completeness_report.";
1800
+ /**
1801
+ * v2 token diet, s3c — ultrathin-form `manual_grounding`, derived from the
1802
+ * s3b minimal form (so `total_entries`, the hoisted sha and the ungrouped
1803
+ * guard are byte-identical by construction). Serialization-only cut, never
1804
+ * silent: the (role, chapter, file) group list with per-group counts is
1805
+ * elided; `total_entries` keeps the exact flat count and `groups_ref` is the
1806
+ * executable reference to the full grouping (same input, detail="standard").
1807
+ * Lossless guards (expected never): if the sha was not hoistable or any group
1808
+ * carries `v1_entity_names`, the minimal `groups` list survives inline.
1809
+ */
1810
+ function buildUltrathinGrounding(minimal) {
1811
+ const mustKeepGroups = minimal.manual_commit_sha === undefined ||
1812
+ minimal.groups.some((group) => group.v1_entity_names !== undefined || group.manual_commit_sha !== undefined);
1813
+ const ultrathin = {
1814
+ total_entries: minimal.total_entries,
1815
+ ...(minimal.manual_commit_sha !== undefined
1816
+ ? { manual_commit_sha: minimal.manual_commit_sha }
1817
+ : {}),
1818
+ ...(mustKeepGroups && minimal.groups.length > 0 ? { groups: minimal.groups } : {}),
1819
+ groups_ref: {
1820
+ tool: "prepare_sbd_toe_codegen_context",
1821
+ with: { detail: "standard" },
1822
+ note: GROUNDING_GROUPS_REF_NOTE_ULTRATHIN
1823
+ }
1824
+ };
1825
+ if (minimal.ungrouped)
1826
+ ultrathin.ungrouped = minimal.ungrouped;
1827
+ return ultrathin;
1828
+ }
1829
+ /**
1830
+ * v2 token diet, s3c — trim the completeness report for `detail: "ultrathin"`.
1831
+ * Every COUNT survives verbatim (never-silent backbone: expected/returned per
1832
+ * entity kind, m_recall, named/unnamed, evidence total/returned/capped/cap and
1833
+ * the rest-ref); only the two diagnostic TEXT arrays are re-encoded as exact
1834
+ * counts + the executable `v1_diagnostics_ref` (see
1835
+ * {@link UltrathinCompletenessReport} for what is cut and why).
1836
+ */
1837
+ function trimCompletenessForUltrathin(report) {
1838
+ const { v1_consistency_mismatches, v1_manifest_warnings, ...kept } = report;
1839
+ return {
1840
+ ...kept,
1841
+ v1_consistency_mismatches_count: v1_consistency_mismatches.length,
1842
+ v1_manifest_warnings_count: v1_manifest_warnings.length,
1843
+ ...(v1_consistency_mismatches.length + v1_manifest_warnings.length > 0
1844
+ ? {
1845
+ v1_diagnostics_ref: {
1846
+ tool: "prepare_sbd_toe_codegen_context",
1847
+ with: { detail: "minimal" },
1848
+ note: V1_DIAGNOSTICS_REF_NOTE
1849
+ }
1850
+ }
1851
+ : {})
1852
+ };
1853
+ }
1854
+ /** Slice-grouped, name-only entity encoding (see {@link SliceGroupedEntityNames}). */
1855
+ function groupEntitiesBySlice(entities) {
1856
+ const grouped = {};
1857
+ for (const entity of entities) {
1858
+ (grouped[entity.slice_id] ??= {})[entity.entity_id] = entity.name ?? null;
1859
+ }
1860
+ return grouped;
1861
+ }
1862
+ /** `category` is derivable iff it equals the requirement_id category segment
1863
+ * (the one before the number — `REQ-AGN-001` → `AGN`; grammar v1.10 §1.18,
1864
+ * single source `requirementCategoryOf`). Bundle-wide invariant, guarded per item. */
1865
+ function categoryIsDerivable(requirementId, category) {
1866
+ const derived = requirementCategoryOf(requirementId);
1867
+ return derived !== undefined && derived === category;
1868
+ }
1869
+ /** Dieted requirements: `source`/derivable `category` elided, verbatim
1870
+ * published `description` appended (s3 — the "how"). s3c: at
1871
+ * `detail: "ultrathin"` (`includeDescriptions: false`) the description is
1872
+ * elided too — each item is exactly {requirement_id, name, type} (plus the
1873
+ * unchanged `category` lossless guard) with the executable
1874
+ * `activated_scope.descriptions_ref` pointing at detail="minimal". */
1875
+ function dietRequirements(requirements, includeDescriptions) {
1876
+ const descriptionById = new Map();
1877
+ if (includeDescriptions) {
1878
+ for (const requirement of getOntologyData().requirements) {
1879
+ if (requirement.description) {
1880
+ descriptionById.set(requirement.requirement_id, requirement.description);
1881
+ }
1882
+ }
1883
+ }
1884
+ return requirements.map((item) => {
1885
+ const { source: _source, category, ...rest } = item;
1886
+ const description = descriptionById.get(item.requirement_id);
1887
+ return {
1888
+ ...rest,
1889
+ ...(categoryIsDerivable(item.requirement_id, category) ? {} : { category }),
1890
+ ...(description ? { description } : {})
1891
+ };
1892
+ });
1893
+ }
1894
+ /** Dieted controls: `source` elided; `direct` controls carry the verbatim
1895
+ * published `description` (s3 — the "how"). s3c: at `detail: "ultrathin"`
1896
+ * (`includeDescriptions: false`) the description is elided — each item is
1897
+ * exactly {control_id, name, domain, control_type, confidence} (every
1898
+ * non-description published field: small, useful, and required to keep the
1899
+ * item more than id-only). */
1900
+ function dietControls(controls, includeDescriptions) {
1901
+ const descriptionById = new Map();
1902
+ if (includeDescriptions) {
1903
+ for (const control of getOntologyData().controls) {
1904
+ if (control.description)
1905
+ descriptionById.set(control.control_id, control.description);
1906
+ }
1907
+ }
1908
+ return controls.map((item) => {
1909
+ const { source: _source, ...rest } = item;
1910
+ const description = item.confidence === "direct" ? descriptionById.get(item.control_id) : undefined;
1911
+ return { ...rest, ...(description ? { description } : {}) };
1912
+ });
1913
+ }
1914
+ /**
1915
+ * v2 token diet, s1+s2+s3 — dieted encoding for `detail: "standard" |
1916
+ * "minimal"`. Pure post-processing over the byte-identical full result. The
1917
+ * citable ID set is EXACTLY the full one (invariant 3; the omitted evidence
1918
+ * patterns carry no citation_map ids — verified by tests). Every cut is
1919
+ * either a lossless derivable-field re-encoding documented in the
1920
+ * codegen-instructions resource legend, or an explicit bound with
1921
+ * total/returned/omitted counts plus an executable reference to the rest
1922
+ * (invariant 2 — never silent):
1923
+ * - s1: inverted citations, grouped grounding, per-item `source` legend;
1924
+ * - s2: relations on-demand via `relations_ref` (include_relations restores);
1925
+ * - s3: evidence cap 25→10 (deterministic prefix; counts + rest-ref in
1926
+ * completeness_report), instructions/template → MCP resource, trace only
1927
+ * with debug, verbatim published `description` on requirements + direct
1928
+ * controls, and derivable-field dedup (category, entity_type/slice_family
1929
+ * via slice-grouped entity maps, relevance_score, citation id repeats);
1930
+ * - s3b (revised ADENDA 2026-07-05 — no top-N): `minimal` keeps the
1931
+ * activated scope byte-identical to `standard` and diverges ONLY on
1932
+ * traceability serialization — evidence cap 10→5 (same mechanism) and
1933
+ * `manual_grounding` in the minimal form (counts + hoisted sha +
1934
+ * executable groups_ref);
1935
+ * - s3c (`ultrathin`, operator reactivation 2026-07-05): same complete
1936
+ * activated set, but descriptions elided (descriptions_ref →
1937
+ * detail="minimal"), evidence cap 5→0 (rest-ref → detail="minimal"),
1938
+ * grounding aggregate-only and completeness diagnostics as counts + ref.
1939
+ */
1940
+ function applyStructuralDiet(result, detail, includeRelations) {
1941
+ // s3/s3b/s3c evidence cap (standard 10, minimal 5, ultrathin 0):
1942
+ // deterministic prefix of the classic (already sorted: relevance_score desc,
1943
+ // id asc) list; each dieted list is by construction a prefix of the next
1944
+ // level's. Never-silent counts below; the rest-ref points to detail="full"
1945
+ // (classic top-25) at standard/minimal and to detail="minimal" (the
1946
+ // CHEAPEST level that returns patterns inline) at ultrathin.
1947
+ const ultrathin = detail === "ultrathin";
1948
+ const evidenceCap = ultrathin
1949
+ ? ULTRATHIN_EVIDENCE_PATTERN_CAP
1950
+ : detail === "minimal"
1951
+ ? MINIMAL_EVIDENCE_PATTERN_CAP
1952
+ : STANDARD_EVIDENCE_PATTERN_CAP;
1953
+ const evidenceKept = result.g2_context.evidence_patterns.slice(0, evidenceCap);
1954
+ const evidenceTotal = result.completeness_report.evidence_patterns_total;
1955
+ const evidenceCapped = evidenceTotal - evidenceKept.length;
1956
+ const completeness = {
1957
+ ...result.completeness_report,
1958
+ evidence_patterns_returned: evidenceKept.length,
1959
+ evidence_patterns_capped: evidenceCapped,
1960
+ evidence_pattern_cap: evidenceCap,
1961
+ ...(evidenceCapped > 0
1962
+ ? {
1963
+ evidence_patterns_rest: (ultrathin
1964
+ ? {
1965
+ tool: "prepare_sbd_toe_codegen_context",
1966
+ with: { detail: "minimal" },
1967
+ note: EVIDENCE_PATTERNS_REST_NOTE_ULTRATHIN
1968
+ }
1969
+ : {
1970
+ tool: "prepare_sbd_toe_codegen_context",
1971
+ with: { detail: "full" },
1972
+ note: "Re-call with the same input at detail='full' for the classic inline " +
1973
+ `top-${EVIDENCE_PATTERN_CAP} evidence_patterns (this list is its ` +
1974
+ "deterministic prefix); with debug=true, ids beyond the classic cap " +
1975
+ "are listed in debug.rejected_candidates."
1976
+ })
1977
+ }
1978
+ : {})
1979
+ };
1980
+ // s3 instructions → resource: conditions computed from the SAME resolved
1981
+ // inputs the core used, so resource + active_conditions reconstruct the
1982
+ // inline full content byte-identically.
1983
+ const echoedRisk = result.input_echo.risk_level;
1984
+ const riskLevel = echoedRisk === "L1" || echoedRisk === "L2" || echoedRisk === "L3"
1985
+ ? echoedRisk
1986
+ : undefined;
1987
+ const instructionsRef = {
1988
+ resource: codegenInstructionsResourceUri(result.mode),
1989
+ active_conditions: activeInstructionConditions({
1990
+ hasOverlay: result.activated_scope.regulatory_obligations.length > 0,
1991
+ riskLevel,
1992
+ citationMapEmpty: Object.keys(result.citation_map).length === 0
1993
+ }),
1994
+ note: "Read this MCP resource for llm_codegen_instructions (slots filtered by " +
1995
+ "active_conditions) and security_rationale_template (task = " +
1996
+ "input_echo.task trimmed) — byte-identical to the detail=full inline " +
1997
+ "content — plus the detail_encoding legend for this payload."
1998
+ };
1999
+ const dieted = {
2000
+ status: result.status,
2001
+ mode: result.mode,
2002
+ // Echo the requested detail (and the include_relations escape hatch, when
2003
+ // active) for audit; the FULL result never echoes either (explicit "full"
2004
+ // must stay byte-identical to the omitted form).
2005
+ input_echo: {
2006
+ ...result.input_echo,
2007
+ detail,
2008
+ ...(includeRelations ? { include_relations: true } : {})
2009
+ },
2010
+ // s3: activation_trace only with debug=true; never-silent counter otherwise
2011
+ // (s3c: ultrathin carries its own note — the standard/minimal text is
2012
+ // byte-frozen by the golden snapshots).
2013
+ ...(result.debug
2014
+ ? { activation_trace: result.activation_trace }
2015
+ : {
2016
+ activation_trace_ref: {
2017
+ entries: result.activation_trace.length,
2018
+ note: ultrathin
2019
+ ? ACTIVATION_TRACE_REF_NOTE_ULTRATHIN
2020
+ : "activation_trace elided at detail=standard/minimal — re-call with " +
2021
+ "debug=true to include it (always inline at detail=full)."
2022
+ }
2023
+ }),
2024
+ provenance_legend: ultrathin ? PROVENANCE_LEGEND_ULTRATHIN : PROVENANCE_LEGEND,
2025
+ // s3c: ultrathin elides the published descriptions (executable
2026
+ // descriptions_ref → detail="minimal"); the lists stay COMPLETE.
2027
+ activated_scope: {
2028
+ requirements: dietRequirements(result.activated_scope.requirements, !ultrathin),
2029
+ controls: dietControls(result.activated_scope.controls, !ultrathin),
2030
+ slices: stripSource(result.activated_scope.slices),
2031
+ regulatory_obligations: stripSource(result.activated_scope.regulatory_obligations),
2032
+ ...(ultrathin
2033
+ ? {
2034
+ descriptions_ref: {
2035
+ tool: "prepare_sbd_toe_codegen_context",
2036
+ with: { detail: "minimal" },
2037
+ note: DESCRIPTIONS_REF_NOTE
2038
+ }
2039
+ }
2040
+ : {})
2041
+ },
2042
+ g2_context: {
2043
+ control_objectives: groupEntitiesBySlice(result.g2_context.control_objectives),
2044
+ mechanisms: groupEntitiesBySlice(result.g2_context.mechanisms),
2045
+ practices: groupEntitiesBySlice(result.g2_context.practices),
2046
+ artifacts: groupEntitiesBySlice(result.g2_context.artifacts),
2047
+ ...(includeRelations
2048
+ ? { relations: stripSource(result.g2_context.relations) }
2049
+ : { relations_ref: buildRelationsRef(result) }),
2050
+ evidence_patterns: evidenceKept.map(({ source: _source, relevance_score: _score, ...rest }) => rest)
2051
+ },
2052
+ // s3b: minimal serves the count+provenance form (executable groups_ref);
2053
+ // standard keeps the full grouping; s3c: ultrathin serves the aggregate
2054
+ // form only (total + hoisted sha + groups_ref, group list elided).
2055
+ manual_grounding: ultrathin
2056
+ ? buildUltrathinGrounding(buildMinimalGrounding(groupManualGrounding(result)))
2057
+ : detail === "minimal"
2058
+ ? buildMinimalGrounding(groupManualGrounding(result))
2059
+ : groupManualGrounding(result),
2060
+ regulatory_overlay: {
2061
+ frameworks: stripSource(result.regulatory_overlay.frameworks),
2062
+ obligations: stripSource(result.regulatory_overlay.obligations),
2063
+ mappings: stripSource(result.regulatory_overlay.mappings),
2064
+ playbooks: stripSource(result.regulatory_overlay.playbooks)
2065
+ },
2066
+ citations: invertCitationMap(result.citation_map),
2067
+ // s3c: ultrathin trims the diagnostic text arrays to exact counts + ref.
2068
+ completeness_report: ultrathin
2069
+ ? trimCompletenessForUltrathin(completeness)
2070
+ : completeness,
2071
+ codegen_instructions_ref: instructionsRef,
2072
+ // s4: identical re-call is deterministic — point the client back at the
2073
+ // context it already holds (full stays byte-identical: no hint there).
2074
+ repeat_call_hint: REPEAT_CALL_HINT,
2075
+ provenance: result.provenance
2076
+ };
2077
+ if (result.debug)
2078
+ dieted.debug = result.debug;
2079
+ return dieted;
2080
+ }
979
2081
  export function handlePrepareCodegenContext(raw) {
980
- // RF-H: append the advisory band (status-aware, pure) around the deterministic result.
2082
+ // v2 token diet (s1/s2): `detail` and `include_relations` select the
2083
+ // response ENCODING only — they are validated up-front and never influence
2084
+ // activation/resolution.
2085
+ const detail = parseDetail(raw);
2086
+ const includeRelations = parseIncludeRelations(raw);
981
2087
  const result = prepareCodegenContextCore(raw);
982
- return { ...result, next: prepareCodegenAffordances(result.status) };
2088
+ const shaped = detail !== "full" && result.status === "ready_for_codegen"
2089
+ ? applyStructuralDiet(result, detail, includeRelations)
2090
+ : result;
2091
+ // RF-H: append the advisory band (status-aware, pure) around the deterministic result.
2092
+ return { ...shaped, next: prepareCodegenAffordances(result.status) };
983
2093
  }
984
2094
  function prepareCodegenContextCore(raw) {
985
2095
  const input = normalizeInput(raw);
@@ -1004,7 +2114,11 @@ function prepareCodegenContextCore(raw) {
1004
2114
  : deterministicConcerns.length > 0
1005
2115
  ? deterministicConcerns
1006
2116
  : activation.concerns;
1007
- const estimatedRequirements = estimateV0RequirementCount(input.risk_level, focusConcerns);
2117
+ void focusConcerns; // kept for the debug notes below; the gate no longer counts requirements
2118
+ // MP1 selection (G-mp1a O2): the engine composes baseline ∪ context and narrows
2119
+ // by the task's declared signals — this is the requirement set served.
2120
+ const selection = runSelectionWithActivation(input, activation);
2121
+ const estimatedRequirements = selection.selected.length;
1008
2122
  const postGate = gateAfterActivation({
1009
2123
  input,
1010
2124
  activation,
@@ -1036,9 +2150,12 @@ function prepareCodegenContextCore(raw) {
1036
2150
  ], activation.trace, { rejected: activation.rejected, notes: activation.notes });
1037
2151
  }
1038
2152
  // ----- Resolve activated scope ----------------------------------------
2153
+ const ontologyForSelection = getOntologyData();
2154
+ const selectedIds = new Set(selection.selected.map((r) => r.requirement_id));
1039
2155
  const v0 = resolveRuntimeV0({
1040
2156
  riskLevel: input.risk_level,
1041
- concerns: activation.concerns
2157
+ concerns: activation.concerns,
2158
+ selectedRequirements: ontologyForSelection.requirements.filter((r) => selectedIds.has(r.requirement_id))
1042
2159
  });
1043
2160
  const activatedSlices = resolveActivatedSlices(g2Data, activation.sliceFamilies);
1044
2161
  const activatedSliceIds = new Set(activatedSlices.map((slice) => slice.slice_id));
@@ -1247,6 +2364,17 @@ function prepareCodegenContextCore(raw) {
1247
2364
  returned_artifacts: activatedArtifacts.length,
1248
2365
  named_v1_entities: namedV1,
1249
2366
  unnamed_v1_entities: totalV1 - namedV1,
2367
+ selection: {
2368
+ eligible: selection.eligible_count,
2369
+ selected: selection.selected.length,
2370
+ narrowed_out_categories: selection.narrowed_out.length,
2371
+ narrowed_out_requirements: selection.narrowed_out.reduce((n, g) => n + g.count, 0),
2372
+ narrowed_out_ref: {
2373
+ tool: "select_sbd_toe_requirements",
2374
+ note: "Categorias elegíveis sem sinal na tarefa foram excluídas pelo narrowing MP1 — " +
2375
+ "a lista completa (por categoria, com razão) vem de select_sbd_toe_requirements com o mesmo contexto."
2376
+ }
2377
+ },
1250
2378
  v1_consistency_mismatches: g2Data.consistency.mismatches,
1251
2379
  v1_manifest_warnings: g2Data.consistency.warnings,
1252
2380
  evidence_patterns_total: scoredEvidencePatterns.length,
@@ -1262,31 +2390,7 @@ function prepareCodegenContextCore(raw) {
1262
2390
  hasOverlay: overlayResolution.activatedObligations.length > 0,
1263
2391
  riskLevel: input.risk_level
1264
2392
  });
1265
- const security_rationale_template = {
1266
- task: input.taskTrimmed,
1267
- decisions: [
1268
- {
1269
- decision: "<fill: what design choice was made>",
1270
- rationale: "<fill: why, citing IDs from citation_map>",
1271
- cited_ids: ["<requirement_id|control_id|slice_id|obligation_id>"]
1272
- }
1273
- ],
1274
- validations: [
1275
- {
1276
- surface: "<fill: code path being validated>",
1277
- rule: "<fill: validation rule>",
1278
- rejection_behaviour: "<fill: how invalid input is rejected>"
1279
- }
1280
- ],
1281
- expected_evidence: [
1282
- {
1283
- artefact: "<fill: test, log, doc, sbom, scan, attestation, ...>",
1284
- location: "<fill: where to find it>",
1285
- verifies: "<fill: which control/requirement id>"
1286
- }
1287
- ],
1288
- residual_risk: "<fill: anything NOT addressed by this change>"
1289
- };
2393
+ const security_rationale_template = buildSecurityRationaleTemplate(input.taskTrimmed);
1290
2394
  const result = {
1291
2395
  status: "ready_for_codegen",
1292
2396
  mode: input.mode,