renma 0.5.1 → 0.6.1

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 (44) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +29 -6
  3. package/dist/commands/catalog.d.ts.map +1 -1
  4. package/dist/commands/catalog.js +3 -0
  5. package/dist/commands/catalog.js.map +1 -1
  6. package/dist/commands/readiness.d.ts.map +1 -1
  7. package/dist/commands/readiness.js +57 -18
  8. package/dist/commands/readiness.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +63 -5
  11. package/dist/config.js.map +1 -1
  12. package/dist/diagnostic-ids.d.ts +88 -0
  13. package/dist/diagnostic-ids.d.ts.map +1 -0
  14. package/dist/diagnostic-ids.js +87 -0
  15. package/dist/diagnostic-ids.js.map +1 -0
  16. package/dist/freshness.d.ts +9 -0
  17. package/dist/freshness.d.ts.map +1 -0
  18. package/dist/freshness.js +27 -0
  19. package/dist/freshness.js.map +1 -0
  20. package/dist/metadata.d.ts.map +1 -1
  21. package/dist/metadata.js +26 -0
  22. package/dist/metadata.js.map +1 -1
  23. package/dist/model.d.ts +3 -0
  24. package/dist/model.d.ts.map +1 -1
  25. package/dist/repeated-context.d.ts +2 -1
  26. package/dist/repeated-context.d.ts.map +1 -1
  27. package/dist/repeated-context.js +11 -10
  28. package/dist/repeated-context.js.map +1 -1
  29. package/dist/rules.d.ts.map +1 -1
  30. package/dist/rules.js +119 -43
  31. package/dist/rules.js.map +1 -1
  32. package/dist/scanner.d.ts.map +1 -1
  33. package/dist/scanner.js +52 -9
  34. package/dist/scanner.js.map +1 -1
  35. package/dist/security-diagnostics.d.ts.map +1 -1
  36. package/dist/security-diagnostics.js +27 -34
  37. package/dist/security-diagnostics.js.map +1 -1
  38. package/dist/suppressions.d.ts +9 -0
  39. package/dist/suppressions.d.ts.map +1 -0
  40. package/dist/suppressions.js +84 -0
  41. package/dist/suppressions.js.map +1 -0
  42. package/dist/types.d.ts +10 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +10 -7
package/dist/rules.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { addDaysIsoDate, isIsoDate, parseDayDuration, todayIsoDate, } from "./freshness.js";
4
+ import { DIAGNOSTIC_IDS } from "./diagnostic-ids.js";
3
5
  import { runRuleRegistry } from "./rule-engine.js";
4
6
  const SECRET_PATTERN = /\b(?:password|passwd|token|api[_-]?key|secret|credential|private[_-]?key)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{8,})/i;
5
7
  const PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/;
@@ -184,9 +186,83 @@ function catalogDeclaredReferenceGovernanceFindings(catalog) {
184
186
  ...unknownReferenceFindings(catalog.dependencies, resolver),
185
187
  ...referenceDeprecatedAssetFindings(catalog.dependencies, resolver),
186
188
  ...orphanedContextAssetFindings(catalog.entries, catalog.dependencies, resolver),
189
+ ...freshnessGovernanceFindings(catalog.entries),
187
190
  ];
188
191
  return findings;
189
192
  }
193
+ function freshnessGovernanceFindings(entries, today = todayIsoDate()) {
194
+ return entries
195
+ .filter((entry) => entry.kind === "context" || entry.kind === "skill")
196
+ .flatMap((entry) => [
197
+ ...expiredAssetFindings(entry, today),
198
+ ...reviewOverdueAssetFindings(entry, today),
199
+ ]);
200
+ }
201
+ function expiredAssetFindings(entry, today) {
202
+ const expiresAt = entry.metadata.expiresAt;
203
+ if (!expiresAt || !isIsoDate(expiresAt) || expiresAt >= today)
204
+ return [];
205
+ return [
206
+ {
207
+ id: DIAGNOSTIC_IDS.MAINT_ASSET_EXPIRED,
208
+ title: "Asset freshness metadata is expired",
209
+ category: "maintenance",
210
+ severity: "medium",
211
+ confidence: "high",
212
+ evidence: metadataFieldFindingEvidence(entry, "expires_at", `expires_at: ${expiresAt}`),
213
+ whyItMatters: "Freshness metadata is a human review contract. Expired assets may contain guidance that has not been intentionally revalidated for current use.",
214
+ remediation: "Review the asset with a human owner, then update expires_at, last_reviewed_at, review_cycle, status, or dependent references as appropriate.",
215
+ constraints: [
216
+ "Do not infer freshness from file modification time.",
217
+ "Do not introduce runtime context selection.",
218
+ "Do not create prompt packages.",
219
+ ],
220
+ verificationSteps: [
221
+ "Run renma scan.",
222
+ "Run renma catalog.",
223
+ "Confirm the freshness metadata reflects human review.",
224
+ ],
225
+ llmHint: `Review ${entry.sourcePath} for stale assumptions, then update explicit freshness metadata only after human review.`,
226
+ },
227
+ ];
228
+ }
229
+ function reviewOverdueAssetFindings(entry, today) {
230
+ const lastReviewedAt = entry.metadata.lastReviewedAt;
231
+ const reviewCycle = entry.metadata.reviewCycle;
232
+ if (!lastReviewedAt || !reviewCycle)
233
+ return [];
234
+ if (!isIsoDate(lastReviewedAt))
235
+ return [];
236
+ const cycleDays = parseDayDuration(reviewCycle);
237
+ if (cycleDays === undefined)
238
+ return [];
239
+ const dueAt = addDaysIsoDate(lastReviewedAt, cycleDays);
240
+ if (dueAt >= today)
241
+ return [];
242
+ return [
243
+ {
244
+ id: DIAGNOSTIC_IDS.MAINT_ASSET_REVIEW_OVERDUE,
245
+ title: "Asset freshness review is overdue",
246
+ category: "maintenance",
247
+ severity: "medium",
248
+ confidence: "high",
249
+ evidence: metadataFieldFindingEvidence(entry, "last_reviewed_at", `last_reviewed_at: ${lastReviewedAt}; review_cycle: ${reviewCycle}`),
250
+ whyItMatters: "Review cycles make human freshness expectations explicit. Overdue assets may no longer match current product, policy, or workflow behavior.",
251
+ remediation: "Review the asset with a human owner, then update last_reviewed_at or review_cycle if the guidance is still current.",
252
+ constraints: [
253
+ "Do not infer freshness from Git history or file modification time.",
254
+ "Do not introduce runtime context selection.",
255
+ "Do not create prompt packages.",
256
+ ],
257
+ verificationSteps: [
258
+ "Run renma scan.",
259
+ "Run renma catalog.",
260
+ "Confirm the next review date is not overdue.",
261
+ ],
262
+ llmHint: `The next review date for ${entry.sourcePath} was ${dueAt}. Revalidate the asset before updating freshness metadata.`,
263
+ },
264
+ ];
265
+ }
190
266
  function duplicateAssetIdFindings(entries) {
191
267
  const entriesById = new Map();
192
268
  for (const entry of entries) {
@@ -197,7 +273,7 @@ function duplicateAssetIdFindings(entries) {
197
273
  return [];
198
274
  const paths = duplicates.map((entry) => entry.sourcePath).sort();
199
275
  return duplicates.map((entry) => ({
200
- id: "META-DUPLICATE-ASSET-ID",
276
+ id: DIAGNOSTIC_IDS.META_DUPLICATE_ASSET_ID,
201
277
  title: "Duplicate asset id",
202
278
  category: "maintenance",
203
279
  severity: "medium",
@@ -228,7 +304,7 @@ function unknownReferenceFindings(dependencies, resolver) {
228
304
  return [];
229
305
  return [
230
306
  {
231
- id: "META-UNKNOWN-REFERENCE",
307
+ id: DIAGNOSTIC_IDS.META_UNKNOWN_REFERENCE,
232
308
  title: "Declared reference does not resolve to a known asset",
233
309
  category: "maintenance",
234
310
  severity: "medium",
@@ -264,7 +340,7 @@ function referenceDeprecatedAssetFindings(dependencies, resolver) {
264
340
  }
265
341
  return [
266
342
  {
267
- id: "MAINT-REFERENCE-DEPRECATED-ASSET",
343
+ id: DIAGNOSTIC_IDS.MAINT_REFERENCE_DEPRECATED_ASSET,
268
344
  title: "Declared reference targets a deprecated or archived asset",
269
345
  category: "maintenance",
270
346
  severity: "medium",
@@ -311,7 +387,7 @@ function orphanedContextAssetFindings(entries, dependencies, resolver) {
311
387
  return [];
312
388
  return [
313
389
  {
314
- id: "MAINT-ORPHANED-CONTEXT-ASSET",
390
+ id: DIAGNOSTIC_IDS.MAINT_ORPHANED_CONTEXT_ASSET,
315
391
  title: "Shared context asset is not referenced by other assets",
316
392
  category: "maintenance",
317
393
  severity: "low",
@@ -392,10 +468,10 @@ export function severityMeets(value, threshold) {
392
468
  function secretFindings(document) {
393
469
  return matchingLineFindings(document, (line) => {
394
470
  if (PRIVATE_KEY_PATTERN.test(line)) {
395
- return finding("SEC-PRIVATE-KEY", "Private key material appears in repository text", "safety", "critical", document, "Remove the key, rotate it if real, and keep only setup instructions or placeholders.");
471
+ return finding(DIAGNOSTIC_IDS.SEC_PRIVATE_KEY, "Private key material appears in repository text", "safety", "critical", document, "Remove the key, rotate it if real, and keep only setup instructions or placeholders.");
396
472
  }
397
473
  if (SECRET_PATTERN.test(line) && !isPlaceholder(line)) {
398
- return finding("SEC-LITERAL-SECRET", "Literal credential-like value appears in repository text", "safety", "high", document, "Move secrets to user-approved inputs or a secret manager, and keep only placeholders in repository files.");
474
+ return finding(DIAGNOSTIC_IDS.SEC_LITERAL_SECRET, "Literal credential-like value appears in repository text", "safety", "high", document, "Move secrets to user-approved inputs or a secret manager, and keep only placeholders in repository files.");
399
475
  }
400
476
  return undefined;
401
477
  });
@@ -406,13 +482,13 @@ function commandFindings(document) {
406
482
  return undefined;
407
483
  if (DESTRUCTIVE_PATTERN.test(line) &&
408
484
  !hasNearbyConfirmation(document.lines, line)) {
409
- return finding("SEC-DESTRUCTIVE-COMMAND", "Dangerous command lacks explicit confirmation or recovery guard", "safety", "high", document, "Require explicit user confirmation, add dry-run/backup guidance, and describe rollback or verification.");
485
+ return finding(DIAGNOSTIC_IDS.SEC_DESTRUCTIVE_COMMAND, "Dangerous command lacks explicit confirmation or recovery guard", "safety", "high", document, "Require explicit user confirmation, add dry-run/backup guidance, and describe rollback or verification.");
410
486
  }
411
487
  if (REMOTE_PATTERN.test(line)) {
412
- return finding("SEC-REMOTE-DEFAULT", "Remote command example uses unsafe default", "safety", "medium", document, "Avoid production placeholders, insecure transport flags, and pipe-to-shell patterns unless paired with verification and confirmation.");
488
+ return finding(DIAGNOSTIC_IDS.SEC_REMOTE_DEFAULT, "Remote command example uses unsafe default", "safety", "medium", document, "Avoid production placeholders, insecure transport flags, and pipe-to-shell patterns unless paired with verification and confirmation.");
413
489
  }
414
490
  if (ENV_COPY_PATTERN.test(line)) {
415
- return finding("SEC-ENV-COPY", "Command may pass broad environment into subprocess execution", "safety", "medium", document, "Pass only required environment variables to subprocesses and avoid forwarding secrets by default.");
491
+ return finding(DIAGNOSTIC_IDS.SEC_ENV_COPY, "Command may pass broad environment into subprocess execution", "safety", "medium", document, "Pass only required environment variables to subprocesses and avoid forwarding secrets by default.");
416
492
  }
417
493
  return undefined;
418
494
  });
@@ -425,17 +501,17 @@ function shapeFindings(document) {
425
501
  const description = document.metadata.description ?? "";
426
502
  const tokenCount = approximateTokenCount(document.artifact.content);
427
503
  if (!description) {
428
- findings.push(documentFinding(document, "QUAL-MISSING-DESCRIPTION", "Skill is missing an explicit description", "quality", "medium", "Add frontmatter description so agents can route to the skill intentionally."));
504
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_DESCRIPTION, "Skill is missing an explicit description", "quality", "medium", "Add frontmatter description so agents can route to the skill intentionally."));
429
505
  }
430
506
  else if (document.artifact.kind === "skill" &&
431
507
  description.length < DESCRIPTION_MIN_CHARS) {
432
- findings.push(documentFinding(document, "QUAL-SHORT-DESCRIPTION", "Skill description is too short for routing clarity", "quality", "low", `Expand frontmatter description to at least ${DESCRIPTION_MIN_CHARS} characters with usage routing guidance.`));
508
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SHORT_DESCRIPTION, "Skill description is too short for routing clarity", "quality", "low", `Expand frontmatter description to at least ${DESCRIPTION_MIN_CHARS} characters with usage routing guidance.`));
433
509
  }
434
510
  const reusableContextFinding = reusableContextCandidateFinding(document, tokenCount);
435
511
  if (reusableContextFinding)
436
512
  findings.push(reusableContextFinding);
437
513
  if (document.artifact.kind === "skill" && tokenCount > SKILL_TOKEN_LIMIT) {
438
- findings.push(documentFinding(document, "QUAL-SKILL-TOKEN-BUDGET", "Skill entrypoint exceeds token budget", "quality", "medium", `Keep SKILL.md under about ${SKILL_TOKEN_LIMIT} tokens as a compact usage guide. Move detailed procedures into reference files, but preserve them losslessly in ordered parts when needed. Do not delete, summarize, or merge away procedural steps. SKILL.md should reference every required support file or index without embedding the full procedure.`, {
514
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SKILL_TOKEN_BUDGET, "Skill entrypoint exceeds token budget", "quality", "medium", `Keep SKILL.md under about ${SKILL_TOKEN_LIMIT} tokens as a compact usage guide. Move detailed procedures into reference files, but preserve them losslessly in ordered parts when needed. Do not delete, summarize, or merge away procedural steps. SKILL.md should reference every required support file or index without embedding the full procedure.`, {
439
515
  whyItMatters: "Large skills can mix LLM-facing usage guidance with reusable domain knowledge. Skills should remain concise routing contracts and usage guides, while reusable QA heuristics, domain rules, and tool guidance live in independently owned shared context assets.",
440
516
  constraints: [
441
517
  "Do not introduce runtime context resolution.",
@@ -454,23 +530,23 @@ function shapeFindings(document) {
454
530
  }
455
531
  if (document.artifact.kind === "skill" &&
456
532
  USER_LOCAL_PATH_PATTERN.test(text)) {
457
- findings.push(documentFinding(document, "QUAL-USER-LOCAL-PATHS", "Skill uses hardcoded user home paths in instructions", "quality", "medium", "Use repo-relative or environment-agnostic paths in skill instructions. If a local path is unavoidable, parameterize it and avoid hardcoding a user-specific home directory such as `/Users/alice/...` or `/home/alice/...`."));
533
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_USER_LOCAL_PATHS, "Skill uses hardcoded user home paths in instructions", "quality", "medium", "Use repo-relative or environment-agnostic paths in skill instructions. If a local path is unavoidable, parameterize it and avoid hardcoding a user-specific home directory such as `/Users/alice/...` or `/home/alice/...`."));
458
534
  }
459
535
  if (!/do not use for|non-goals|out of scope/.test(text)) {
460
- findings.push(documentFinding(document, "QUAL-MISSING-NEGATIVE-ROUTING", "Skill lacks negative routing guidance", "structure", "medium", "Add a DO NOT USE FOR or non-goals section so agents know when to choose another path."));
536
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_NEGATIVE_ROUTING, "Skill lacks negative routing guidance", "structure", "medium", "Add a DO NOT USE FOR or non-goals section so agents know when to choose another path."));
461
537
  }
462
538
  if (!/use this skill|when to use|trigger|routing|context route|mixin/.test(text)) {
463
- findings.push(documentFinding(document, "QUAL-MISSING-ROUTING-CLARITY", "Skill lacks routing clarity", "quality", "low", "Add concise routing language: when to use the skill, whether it invokes other skills, or whether it is a utility skill for single operations."));
539
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_ROUTING_CLARITY, "Skill lacks routing clarity", "quality", "low", "Add concise routing language: when to use the skill, whether it invokes other skills, or whether it is a utility skill for single operations."));
464
540
  }
465
541
  if (!/example|input|output/.test(text)) {
466
- findings.push(documentFinding(document, "QUAL-MISSING-EXAMPLES", "Skill lacks examples", "quality", "low", "Add examples that show representative inputs, outputs, or behavior."));
542
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_EXAMPLES, "Skill lacks examples", "quality", "low", "Add examples that show representative inputs, outputs, or behavior."));
467
543
  }
468
544
  if (!/preflight|before you begin|first check|prerequisite|context/.test(text)) {
469
- findings.push(documentFinding(document, "QUAL-MISSING-PREFLIGHT", "Skill lacks a preflight step", "quality", "medium", "Add a preflight section that captures environment, permissions, target files, and assumptions before acting."));
545
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_PREFLIGHT, "Skill lacks a preflight step", "quality", "medium", "Add a preflight section that captures environment, permissions, target files, and assumptions before acting."));
470
546
  }
471
547
  if (document.artifact.kind === "skill" &&
472
548
  !REQUIRED_INPUTS_PATTERN.test(text)) {
473
- findings.push(documentFinding(document, "QUAL-MISSING-REQUIRED-INPUTS", "Skill does not state required inputs", "quality", "medium", "Add a Required inputs or Prerequisites section that states the user-provided inputs, target files, repository state, permissions, credentials, or environment assumptions needed before the workflow can start.", {
549
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_REQUIRED_INPUTS, "Skill does not state required inputs", "quality", "medium", "Add a Required inputs or Prerequisites section that states the user-provided inputs, target files, repository state, permissions, credentials, or environment assumptions needed before the workflow can start.", {
474
550
  whyItMatters: "Agents need explicit input requirements before starting a workflow. Missing required inputs can cause the agent to guess targets, assume permissions, or start without enough repository context.",
475
551
  constraints: [
476
552
  "Do not infer runtime context.",
@@ -489,7 +565,7 @@ function shapeFindings(document) {
489
565
  }
490
566
  if (document.artifact.kind === "skill" &&
491
567
  !COMPLETION_CRITERIA_PATTERN.test(text)) {
492
- findings.push(documentFinding(document, "QUAL-MISSING-COMPLETION-CRITERIA", "Skill does not state completion criteria", "quality", "medium", "Add a Completion criteria, Success requirements, Deliverables, or Final response section that states the observable outputs or conditions that mean the workflow is complete.", {
568
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_COMPLETION_CRITERIA, "Skill does not state completion criteria", "quality", "medium", "Add a Completion criteria, Success requirements, Deliverables, or Final response section that states the observable outputs or conditions that mean the workflow is complete.", {
493
569
  whyItMatters: "Agents need explicit completion criteria before finishing a workflow. Missing completion criteria can cause incomplete delivery, unnecessary follow-up work, or inconsistent final responses.",
494
570
  constraints: [
495
571
  "Do not infer runtime context.",
@@ -507,11 +583,11 @@ function shapeFindings(document) {
507
583
  }));
508
584
  }
509
585
  if (!/verify|validation|test|confirm result|expected output/.test(text)) {
510
- findings.push(documentFinding(document, "QUAL-MISSING-VERIFICATION", "Skill lacks verification guidance", "quality", "medium", "State how to verify success with a command, check, or observable result."));
586
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_VERIFICATION, "Skill lacks verification guidance", "quality", "medium", "State how to verify success with a command, check, or observable result."));
511
587
  }
512
588
  if (document.headings.length < 2 &&
513
589
  document.artifact.content.split(/\s+/).length > 120) {
514
- findings.push(documentFinding(document, "QUAL-LOW-HEADING-DENSITY", "Long instruction file has few headings", "structure", "low", "Split long prose into task-oriented headings so agents can navigate it reliably."));
590
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_LOW_HEADING_DENSITY, "Long instruction file has few headings", "structure", "low", "Split long prose into task-oriented headings so agents can navigate it reliably."));
515
591
  }
516
592
  return findings;
517
593
  }
@@ -561,7 +637,7 @@ function reusableContextCandidateFinding(document, tokenCount) {
561
637
  : undefined,
562
638
  ].filter((part) => Boolean(part));
563
639
  return {
564
- id: "MAINT-SKILL-REUSABLE-CONTEXT-CANDIDATE",
640
+ id: DIAGNOSTIC_IDS.MAINT_SKILL_REUSABLE_CONTEXT_CANDIDATE,
565
641
  title: "Skill may contain reusable context worth extracting",
566
642
  category: "maintenance",
567
643
  severity: "low",
@@ -647,7 +723,7 @@ function supportSharedContextCandidateFindings(document) {
647
723
  ].join("\n");
648
724
  return [
649
725
  {
650
- id: "MAINT-SUPPORT-ASSET-SHARED-CONTEXT-CANDIDATE",
726
+ id: DIAGNOSTIC_IDS.MAINT_SUPPORT_ASSET_SHARED_CONTEXT_CANDIDATE,
651
727
  title: "Skill-local support file may be a shared context candidate",
652
728
  category: "maintenance",
653
729
  severity: "low",
@@ -688,7 +764,7 @@ function contextPathNonSemanticFindings(document) {
688
764
  return [];
689
765
  return [
690
766
  {
691
- id: "MAINT-CONTEXT-PATH-NON-SEMANTIC",
767
+ id: DIAGNOSTIC_IDS.MAINT_CONTEXT_PATH_NON_SEMANTIC,
692
768
  title: "Context asset path appears process-oriented rather than semantic",
693
769
  category: "maintenance",
694
770
  severity: "low",
@@ -736,7 +812,7 @@ function skillContextReferenceNotDeclaredFindings(document) {
736
812
  return [...matches.entries()]
737
813
  .filter(([referencedPath]) => !declaredContexts.has(referencedPath))
738
814
  .map(([referencedPath, match]) => ({
739
- id: "MAINT-SKILL-CONTEXT-REFERENCE-NOT-DECLARED",
815
+ id: DIAGNOSTIC_IDS.MAINT_SKILL_CONTEXT_REFERENCE_NOT_DECLARED,
740
816
  title: "Skill references a shared context without declaring it",
741
817
  category: "maintenance",
742
818
  severity: "low",
@@ -793,7 +869,7 @@ function skillReferencesSupersededAssetFindings(documents) {
793
869
  ].join("\n");
794
870
  return [
795
871
  {
796
- id: "MAINT-SKILL-REFERENCES-SUPERSEDED-ASSET",
872
+ id: DIAGNOSTIC_IDS.MAINT_SKILL_REFERENCES_SUPERSEDED_ASSET,
797
873
  title: "Skill references a superseded local support asset",
798
874
  category: "maintenance",
799
875
  severity: "low",
@@ -881,7 +957,7 @@ function assetReferencesSupersededAssetFindings(documents) {
881
957
  ].join("\n");
882
958
  return [
883
959
  {
884
- id: "MAINT-ASSET-REFERENCES-SUPERSEDED-ASSET",
960
+ id: DIAGNOSTIC_IDS.MAINT_ASSET_REFERENCES_SUPERSEDED_ASSET,
885
961
  title: "Asset references a superseded support file",
886
962
  category: "maintenance",
887
963
  severity: "low",
@@ -951,7 +1027,7 @@ function contextBudgetFindings(document) {
951
1027
  if (tokenCount <= limit)
952
1028
  return [];
953
1029
  return [
954
- documentFinding(document, "QUAL-SUPPORT-ASSET-TOKEN-BUDGET", "Support asset exceeds token guidance", "quality", "low", `Keep ${document.artifact.kind} assets under about ${limit} tokens where practical. If a file is too large, run \`renma suggest-semantic-split ${document.artifact.path}\` to get a semantic split proposal, then split it losslessly into meaning-based ordered part files. Do not delete, summarize, or merge away procedural steps. The parent file or SKILL.md should reference every part in order, and the split should preserve the original procedure text exactly. Verify by reconstructing the parts and comparing them to the original content before accepting the fix.`, {
1030
+ documentFinding(document, DIAGNOSTIC_IDS.QUAL_SUPPORT_ASSET_TOKEN_BUDGET, "Support asset exceeds token guidance", "quality", "low", `Keep ${document.artifact.kind} assets under about ${limit} tokens where practical. If a file is too large, run \`renma suggest-semantic-split ${document.artifact.path}\` to get a semantic split proposal, then split it losslessly into meaning-based ordered part files. Do not delete, summarize, or merge away procedural steps. The parent file or SKILL.md should reference every part in order, and the split should preserve the original procedure text exactly. Verify by reconstructing the parts and comparing them to the original content before accepting the fix.`, {
955
1031
  whyItMatters: "Oversized support assets are harder for humans and LLM coding agents to review safely. Shared context and local support files should stay modular enough that ownership, scope, and static references remain clear.",
956
1032
  constraints: [
957
1033
  "Do not introduce runtime context resolution.",
@@ -975,7 +1051,7 @@ function profileFindings(document) {
975
1051
  if (/base[_ -]?skill|extends/.test(text))
976
1052
  return [];
977
1053
  return [
978
- documentFinding(document, "PROF-MISSING-BASE", "Profile overlay does not declare its base skill", "structure", "medium", "Declare the base skill or compatibility target so routing conflicts are auditable."),
1054
+ documentFinding(document, DIAGNOSTIC_IDS.PROF_MISSING_BASE, "Profile overlay does not declare its base skill", "structure", "medium", "Declare the base skill or compatibility target so routing conflicts are auditable."),
979
1055
  ];
980
1056
  }
981
1057
  function skillLocalSupportReachabilityFindings(documents) {
@@ -990,7 +1066,7 @@ function skillLocalSupportReachabilityFindings(documents) {
990
1066
  const text = skill.artifact.content.toLowerCase();
991
1067
  const hasLocalSupportGuidance = /support file|local support|context route|context map|mixin|profiles?\/|references?\/|examples?\/|load .*?(?:profile|reference|example)|reference .*?(?:profile|reference|example)/.test(text);
992
1068
  if (!hasLocalSupportGuidance) {
993
- findings.push(documentFinding(skill, "SUPPORT-MISSING-REACHABILITY-GUIDANCE", "Skill has local support files but no reachability guidance", "structure", "medium", "Add local support file reachability guidance so the top-level skill declares when profiles, references, examples, or scripts are reachable. If support content was split into ordered parts, reference the index or all parts in order. Preserve original concrete steps. Do not delete, summarize, or merge away procedural steps.", {
1069
+ findings.push(documentFinding(skill, DIAGNOSTIC_IDS.SUPPORT_MISSING_REACHABILITY_GUIDANCE, "Skill has local support files but no reachability guidance", "structure", "medium", "Add local support file reachability guidance so the top-level skill declares when profiles, references, examples, or scripts are reachable. If support content was split into ordered parts, reference the index or all parts in order. Preserve original concrete steps. Do not delete, summarize, or merge away procedural steps.", {
994
1070
  whyItMatters: "Local support files should be statically discoverable from the skill so humans and LLM coding agents can tell which repository evidence belongs to the skill without relying on runtime context selection.",
995
1071
  constraints: [
996
1072
  "Do not introduce runtime context resolution.",
@@ -1117,7 +1193,7 @@ function disallowedSkillAssetFindings(document, config) {
1117
1193
  const [, skillName = "", assetRoot = "", rest = ""] = match;
1118
1194
  const target = canonicalSkillAssetTarget(config, skillName, assetRoot, rest);
1119
1195
  return [
1120
- documentFinding(document, "LAYOUT-DISALLOWED-SKILL-ASSET", `Disallowed skill-local ${assetRoot} asset`, "structure", assetRoot === "scripts" ? "medium" : "low", `Move this file to \`${target}\` and update every repo-local reference to the new canonical path.`, {
1196
+ documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_DISALLOWED_SKILL_ASSET, `Disallowed skill-local ${assetRoot} asset`, "structure", assetRoot === "scripts" ? "medium" : "low", `Move this file to \`${target}\` and update every repo-local reference to the new canonical path.`, {
1121
1197
  whyItMatters: "Strict layout keeps skills as thin entrypoints, shared context under contexts/, and executable helpers under tools/ so agents can migrate repositories deterministically.",
1122
1198
  verificationSteps: [
1123
1199
  `Move ${document.artifact.path} to ${target}.`,
@@ -1147,7 +1223,7 @@ function thinSkillLayoutFindings(document) {
1147
1223
  ];
1148
1224
  if (wordCount > 450 ||
1149
1225
  procedureSignals.some((pattern) => pattern.test(document.artifact.content))) {
1150
- findings.push(documentFinding(document, "LAYOUT-SKILL-NOT-THIN", "Skill entrypoint contains procedure content", "structure", wordCount > 700 ? "medium" : "low", "Move reusable procedure content into contexts/** and keep SKILL.md as a concise router that points to the required context assets.", {
1226
+ findings.push(documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_NOT_THIN, "Skill entrypoint contains procedure content", "structure", wordCount > 700 ? "medium" : "low", "Move reusable procedure content into contexts/** and keep SKILL.md as a concise router that points to the required context assets.", {
1151
1227
  whyItMatters: "Strict layout expects skills/*/SKILL.md to route agents, not own canonical references, profiles, examples, scripts, or detailed procedures.",
1152
1228
  verificationSteps: [
1153
1229
  "Confirm SKILL.md contains routing guidance and required context references only.",
@@ -1158,7 +1234,7 @@ function thinSkillLayoutFindings(document) {
1158
1234
  }
1159
1235
  const command = firstExecutableCommand(document);
1160
1236
  if (command) {
1161
- findings.push(findingAt(document, "LAYOUT-SKILL-EXECUTABLE-COMMAND", "Skill entrypoint contains executable command", "structure", "low", command.line, command.command, "Move executable setup commands into contexts/** procedures or tools/** helper documentation, and keep SKILL.md as a router.", {
1237
+ findings.push(findingAt(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_EXECUTABLE_COMMAND, "Skill entrypoint contains executable command", "structure", "low", command.line, command.command, "Move executable setup commands into contexts/** procedures or tools/** helper documentation, and keep SKILL.md as a router.", {
1162
1238
  whyItMatters: "Executable setup commands in SKILL.md make the entrypoint procedural and harder to migrate to the strict three-root layout.",
1163
1239
  verificationSteps: [
1164
1240
  "Move the command guidance to a context/reference/procedure.",
@@ -1175,7 +1251,7 @@ function helperCommandFindings(document, root, paths, config) {
1175
1251
  if (!scriptPath)
1176
1252
  continue;
1177
1253
  if (/^skills\/[^/]+\/scripts\//.test(scriptPath)) {
1178
- findings.push(findingAt(document, "PATH-HELPER-COMMAND-SKILL-SCRIPTS", "Helper command points to skill-local scripts", "structure", "medium", command.line, command.command, `Move the helper script to \`${canonicalHelperTarget(config, scriptPath)}\` and update this command to use the tools/** path.`, {
1254
+ findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_SKILL_SCRIPTS, "Helper command points to skill-local scripts", "structure", "medium", command.line, command.command, `Move the helper script to \`${canonicalHelperTarget(config, scriptPath)}\` and update this command to use the tools/** path.`, {
1179
1255
  whyItMatters: "Strict layout keeps executable helper assets under tools/**, not under skills/**.",
1180
1256
  verificationSteps: [
1181
1257
  `Confirm ${canonicalHelperTarget(config, scriptPath)} exists.`,
@@ -1185,7 +1261,7 @@ function helperCommandFindings(document, root, paths, config) {
1185
1261
  continue;
1186
1262
  }
1187
1263
  if (scriptPath.includes("/scripts/") && !scriptPath.startsWith("tools/")) {
1188
- findings.push(findingAt(document, "PATH-HELPER-COMMAND-NON_TOOLS", "Helper command does not use tools root", "structure", "low", command.line, command.command, "Update helper script commands to reference scripts under tools/**.", {
1264
+ findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_NON_TOOLS, "Helper command does not use tools root", "structure", "low", command.line, command.command, "Update helper script commands to reference scripts under tools/**.", {
1189
1265
  whyItMatters: "Helper commands should resolve to non-context helper assets in tools/** so contexts remain LLM-readable guidance.",
1190
1266
  }));
1191
1267
  continue;
@@ -1193,7 +1269,7 @@ function helperCommandFindings(document, root, paths, config) {
1193
1269
  if (scriptPath.startsWith("tools/") &&
1194
1270
  !paths.has(scriptPath) &&
1195
1271
  !(root && existsSync(path.join(root, scriptPath)))) {
1196
- findings.push(findingAt(document, "PATH-HELPER-COMMAND-UNRESOLVED", "Helper command target does not resolve", "structure", "medium", command.line, command.command, `Create \`${scriptPath}\` or update this command to the correct tools/** helper path.`, {
1272
+ findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_UNRESOLVED, "Helper command target does not resolve", "structure", "medium", command.line, command.command, `Create \`${scriptPath}\` or update this command to the correct tools/** helper path.`, {
1197
1273
  whyItMatters: "Agents need helper commands in markdown procedures to resolve deterministically before running them.",
1198
1274
  }));
1199
1275
  }
@@ -1229,7 +1305,7 @@ function layoutConsistencyFindings(document) {
1229
1305
  if (!match)
1230
1306
  continue;
1231
1307
  const line = lineForOffset(text, match.index ?? 0);
1232
- findings.push(findingAt(document, "DOCS-LAYOUT-INCONSISTENT", "Repository docs describe a non-canonical layout", "maintenance", "low", line, lineText(document, line), stale.message, {
1308
+ findings.push(findingAt(document, DIAGNOSTIC_IDS.DOCS_LAYOUT_INCONSISTENT, "Repository docs describe a non-canonical layout", "maintenance", "low", line, lineText(document, line), stale.message, {
1233
1309
  whyItMatters: "README.md and AGENTS.md should teach agents the same strict three-root layout that Renma enforces.",
1234
1310
  verificationSteps: [
1235
1311
  "Update README.md and AGENTS.md to mention skills/, contexts/, and tools/ canonical roots.",
@@ -1242,7 +1318,7 @@ function layoutConsistencyFindings(document) {
1242
1318
  function contextRootFindings(document) {
1243
1319
  if (document.artifact.path.startsWith("context/")) {
1244
1320
  return [
1245
- documentFinding(document, "LAYOUT-CONTEXT-LEGACY-ROOT", "Context asset uses legacy context/ root", "structure", "low", "Move canonical LLM-readable context assets under contexts/**.", {
1321
+ documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_CONTEXT_LEGACY_ROOT, "Context asset uses legacy context/ root", "structure", "low", "Move canonical LLM-readable context assets under contexts/**.", {
1246
1322
  whyItMatters: "The strict repository layout uses contexts/**/*.md as canonical LLM-readable context assets.",
1247
1323
  }),
1248
1324
  ];
@@ -1253,7 +1329,7 @@ function helperRootFindings(document) {
1253
1329
  if (document.artifact.path.includes("/scripts/") &&
1254
1330
  !document.artifact.path.startsWith("tools/")) {
1255
1331
  return [
1256
- documentFinding(document, "LAYOUT-HELPER-NON_TOOLS", "Helper script is outside tools root", "structure", "medium", "Move non-context helper assets under tools/** and update command references.", {
1332
+ documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_HELPER_NON_TOOLS, "Helper script is outside tools root", "structure", "medium", "Move non-context helper assets under tools/** and update command references.", {
1257
1333
  whyItMatters: "The strict repository layout reserves tools/** for executable and non-context helper assets.",
1258
1334
  }),
1259
1335
  ];
@@ -1278,7 +1354,7 @@ function declaredDependencyLayoutFindings(catalog, paths, root) {
1278
1354
  if (!source)
1279
1355
  continue;
1280
1356
  findings.push({
1281
- id: "LAYOUT-CONTEXT-REFERENCE-NON_CANONICAL",
1357
+ id: DIAGNOSTIC_IDS.LAYOUT_CONTEXT_REFERENCE_NON_CANONICAL,
1282
1358
  title: "Declared context path is not under canonical roots",
1283
1359
  category: "structure",
1284
1360
  severity: "low",
@@ -1401,10 +1477,10 @@ function evidence(document, line, snippet) {
1401
1477
  }
1402
1478
  function localSupportUnreachableRuleId(kind) {
1403
1479
  if (kind === "profile")
1404
- return "SUPPORT-UNREACHABLE-PROFILE";
1480
+ return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_PROFILE;
1405
1481
  if (kind === "example")
1406
- return "SUPPORT-UNREACHABLE-EXAMPLE";
1407
- return "SUPPORT-UNREACHABLE-REFERENCE";
1482
+ return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_EXAMPLE;
1483
+ return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_REFERENCE;
1408
1484
  }
1409
1485
  function escapeRegExp(value) {
1410
1486
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");