session-orchestrator 3.17.0 → 3.20.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 (221) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +185 -412
  6. package/README.md +12 -9
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/commands/session.md +6 -2
  21. package/docs/USER-GUIDE.md +9 -4
  22. package/docs/ci-setup.md +121 -7
  23. package/docs/codex-setup.md +1 -1
  24. package/docs/components.md +6 -6
  25. package/docs/cursor-setup.md +22 -9
  26. package/docs/events-schema.md +5 -1
  27. package/docs/instruction-delivery.md +794 -0
  28. package/docs/rule-authoring.md +58 -9
  29. package/docs/session-config-reference.md +245 -50
  30. package/docs/session-config-template.md +39 -26
  31. package/hooks/_lib/guard-source-loader.mjs +680 -0
  32. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  33. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  34. package/hooks/config-protection.mjs +0 -0
  35. package/hooks/enforce-commands.mjs +226 -19
  36. package/hooks/enforce-scope.mjs +133 -9
  37. package/hooks/hooks-codex.json +1 -1
  38. package/hooks/hooks-cursor.json +11 -2
  39. package/hooks/hooks-pi.json +10 -0
  40. package/hooks/hooks.json +21 -1
  41. package/hooks/on-session-end.mjs +178 -18
  42. package/hooks/on-session-start.mjs +30 -4
  43. package/hooks/post-bash-write-verify.mjs +977 -0
  44. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  45. package/hooks/pre-bash-destructive-guard.mjs +616 -164
  46. package/hooks/pre-bash-issue-budget.mjs +167 -0
  47. package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
  48. package/hooks/pre-bash-templates-first.mjs +96 -63
  49. package/hooks/subagent-telemetry.mjs +527 -37
  50. package/package.json +6 -3
  51. package/pi/prompts/contract-version-bump.md +12 -0
  52. package/rules/README.md +32 -0
  53. package/scripts/archive-closed-prds.mjs +12 -22
  54. package/scripts/autopilot-multi.mjs +103 -20
  55. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  56. package/scripts/backfill-learnings-from-vault.mjs +967 -0
  57. package/scripts/check-doc-consistency.sh +17 -1
  58. package/scripts/emit-session.mjs +3 -40
  59. package/scripts/eval-session.mjs +50 -9
  60. package/scripts/fleet-instruction-scan.mjs +141 -0
  61. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  62. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  63. package/scripts/lib/backlog-scan.mjs +39 -6
  64. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  65. package/scripts/lib/ci-status-banner.mjs +75 -12
  66. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  67. package/scripts/lib/command-blocker.mjs +1273 -58
  68. package/scripts/lib/config/config-protection.mjs +2 -1
  69. package/scripts/lib/config/drift-check.mjs +9 -1
  70. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  71. package/scripts/lib/config/issue-budget.mjs +123 -0
  72. package/scripts/lib/config/reconcile.mjs +21 -0
  73. package/scripts/lib/config/section-extractor.mjs +121 -1
  74. package/scripts/lib/config-schema.mjs +23 -3
  75. package/scripts/lib/config.mjs +17 -0
  76. package/scripts/lib/convergence-monitor.mjs +49 -3
  77. package/scripts/lib/description-surface.mjs +535 -0
  78. package/scripts/lib/dispatcher/enumerate.mjs +26 -40
  79. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  80. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  81. package/scripts/lib/eval/engine.mjs +47 -5
  82. package/scripts/lib/events.mjs +59 -7
  83. package/scripts/lib/gates/gate-full.mjs +15 -3
  84. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  85. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  86. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  87. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  88. package/scripts/lib/handover-gate.mjs +7 -3
  89. package/scripts/lib/hardening.mjs +9 -9
  90. package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
  91. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  92. package/scripts/lib/io.mjs +345 -10
  93. package/scripts/lib/issue-budget.mjs +269 -0
  94. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  95. package/scripts/lib/label-scope.mjs +47 -0
  96. package/scripts/lib/learnings/affinity.mjs +434 -0
  97. package/scripts/lib/learnings/candidates.mjs +736 -0
  98. package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
  99. package/scripts/lib/learnings/judgment.mjs +782 -0
  100. package/scripts/lib/learnings/kebab.mjs +128 -0
  101. package/scripts/lib/learnings/schema.mjs +43 -3
  102. package/scripts/lib/learnings/select.mjs +550 -0
  103. package/scripts/lib/lock-reaper.mjs +1 -2
  104. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  105. package/scripts/lib/peer-discovery.mjs +645 -0
  106. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  107. package/scripts/lib/product-repo-detect.mjs +9 -8
  108. package/scripts/lib/project-hygiene.mjs +432 -0
  109. package/scripts/lib/quality-gate.mjs +167 -0
  110. package/scripts/lib/recommendations-v0.mjs +1 -1
  111. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  112. package/scripts/lib/reconcile/emitter.mjs +128 -24
  113. package/scripts/lib/reconcile/engine.mjs +156 -54
  114. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  115. package/scripts/lib/reconcile/renderer.mjs +141 -25
  116. package/scripts/lib/reconcile/sanitize.mjs +518 -0
  117. package/scripts/lib/reconcile/writer.mjs +95 -1
  118. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  119. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  120. package/scripts/lib/resource-probe.mjs +19 -0
  121. package/scripts/lib/rule-loader.mjs +6 -0
  122. package/scripts/lib/scope-baseline.mjs +564 -0
  123. package/scripts/lib/scope-gate.mjs +568 -145
  124. package/scripts/lib/session-close-backfill.mjs +63 -8
  125. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  126. package/scripts/lib/session-id.mjs +221 -41
  127. package/scripts/lib/session-lock.mjs +304 -6
  128. package/scripts/lib/session-record-repair.mjs +551 -0
  129. package/scripts/lib/session-schema/constants.mjs +22 -3
  130. package/scripts/lib/session-schema/serializer.mjs +54 -0
  131. package/scripts/lib/session-schema/validator.mjs +16 -0
  132. package/scripts/lib/session-schema.mjs +1 -0
  133. package/scripts/lib/session-token-rollup.mjs +68 -6
  134. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  135. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  136. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  137. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  138. package/scripts/lib/soul-resolve.mjs +12 -0
  139. package/scripts/lib/spiral-carryover.mjs +142 -30
  140. package/scripts/lib/state-md/mission-status.mjs +53 -3
  141. package/scripts/lib/subagents-schema.mjs +43 -9
  142. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  143. package/scripts/lib/tests-src-ratio.mjs +484 -0
  144. package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
  145. package/scripts/lib/validate/check-agents.mjs +56 -0
  146. package/scripts/lib/validate/check-banner-parity.mjs +376 -0
  147. package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
  148. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  149. package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
  150. package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
  151. package/scripts/lib/validate/check-rules.mjs +244 -36
  152. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  153. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  154. package/scripts/lib/validate/check-unwired-features.mjs +549 -0
  155. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  156. package/scripts/lib/vault-archive.mjs +17 -2
  157. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  158. package/scripts/lib/vault-mirror/process.mjs +30 -0
  159. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  160. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  161. package/scripts/lib/wave-resource-gate.mjs +115 -11
  162. package/scripts/lib/worktree/listing.mjs +44 -7
  163. package/scripts/mcp-server.sh +17 -3
  164. package/scripts/measure-context-overhead.sh +151 -0
  165. package/scripts/memory-propose.mjs +72 -9
  166. package/scripts/print-applicable-rules.mjs +218 -16
  167. package/scripts/print-learnings-index.mjs +474 -0
  168. package/scripts/release.mjs +534 -0
  169. package/scripts/repair-invalid-sessions.mjs +209 -0
  170. package/scripts/run-quality-gate.mjs +123 -5
  171. package/scripts/sweep-expired-learnings.mjs +192 -32
  172. package/scripts/validate-plugin.mjs +21 -0
  173. package/scripts/validate-wave-scope.mjs +182 -17
  174. package/scripts/vault-integration-watcher.mjs +32 -10
  175. package/skills/_shared/config-reading.md +2 -2
  176. package/skills/bootstrap/fast-template.md +1 -1
  177. package/skills/brainstorm/soul.md +47 -1
  178. package/skills/claude-md-drift-check/checker.mjs +145 -28
  179. package/skills/contract-version-bump/SKILL.md +219 -0
  180. package/skills/discovery/SKILL.md +4 -4
  181. package/skills/discovery/issue-templates.md +11 -11
  182. package/skills/discovery/probes-audit.md +1 -1
  183. package/skills/discovery/probes-feature.md +1 -1
  184. package/skills/discovery/probes-session.md +26 -5
  185. package/skills/ecosystem-health/SKILL.md +1 -1
  186. package/skills/ecosystem-health/wizard.md +4 -4
  187. package/skills/evolve/SKILL.md +117 -18
  188. package/skills/gitlab-ops/SKILL.md +25 -12
  189. package/skills/gitlab-portfolio/SKILL.md +2 -2
  190. package/skills/grill/soul.md +44 -1
  191. package/skills/hook-development/SKILL.md +1 -1
  192. package/skills/mode-selector/SKILL.md +1 -1
  193. package/skills/npm-publish/SKILL.md +17 -1
  194. package/skills/plan/SKILL.md +5 -5
  195. package/skills/plan/mode-feature.md +4 -4
  196. package/skills/plan/mode-new.md +10 -10
  197. package/skills/plan/mode-retro.md +1 -1
  198. package/skills/plan/soul.md +46 -3
  199. package/skills/quality-gates/SKILL.md +1 -1
  200. package/skills/reconcile/SKILL.md +21 -4
  201. package/skills/session-end/SKILL.md +34 -36
  202. package/skills/session-end/discovery-scan.md +4 -2
  203. package/skills/session-end/drift-operations.md +4 -4
  204. package/skills/session-end/metrics-collection.md +13 -0
  205. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  206. package/skills/session-end/phase-3-6-tail.md +32 -2
  207. package/skills/session-end/plan-verification.md +6 -7
  208. package/skills/session-end/session-metrics-write.md +2 -0
  209. package/skills/session-end/vault-operations.md +1 -1
  210. package/skills/session-end/verification-checklist.md +1 -1
  211. package/skills/session-plan/SKILL.md +6 -2
  212. package/skills/session-plan/wave-template.md +2 -0
  213. package/skills/session-start/SKILL.md +75 -7
  214. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  215. package/skills/session-start/soul.md +41 -1
  216. package/skills/test-runner/SKILL.md +2 -2
  217. package/skills/vault-sync/validator.mjs +108 -7
  218. package/skills/wave-executor/SKILL.md +6 -7
  219. package/skills/wave-executor/circuit-breaker.md +2 -0
  220. package/skills/wave-executor/wave-loop.md +198 -80
  221. package/templates/_shared/loop.md +4 -4
@@ -0,0 +1,128 @@
1
+ /**
2
+ * learnings/kebab.mjs — the slug primitive behind every `learning_key`, and
3
+ * {@link learningKeyOf}, the whole-key derivation built on it.
4
+ *
5
+ * `learning_key` is not a stored field. It is DERIVED as
6
+ * `` `${type}/${kebab(title || subject)}` `` and is the logical identity the
7
+ * whole dedupe/idempotency layer keys on (`reconcile/idempotency.mjs` calls it
8
+ * "THE logical dedupe key"). That makes this a contract, not a formatting
9
+ * helper: two callers that derive differently do not produce two ugly slugs,
10
+ * they FORK the key space — the same learning renders under two identities,
11
+ * dedupe stops firing, and both halves look correct in isolation.
12
+ *
13
+ * Before this module existed there were four copies of `kebab` plus one inline
14
+ * expression, none exported, none shared (two of the four landed on the same
15
+ * day, from two different authors, each of whom flagged the duplication in
16
+ * their own report). They agreed on every reachable input — verified, not
17
+ * assumed — so consolidating here changed no existing key. See the byte-identity
18
+ * proof pinned in `tests/scripts/lib/learnings/kebab.test.mjs`.
19
+ *
20
+ * Consolidating the primitive left the SECOND half of the problem open, and it
21
+ * is the half that bites: what each call site FED the primitive. Four sites
22
+ * derived `${type}/…` verbatim while the writer (`reconcile/emitter.mjs`) alone
23
+ * derived `${kebab(type)}/…` — a divergence invisible while every live `type`
24
+ * is kebab-identical, and silent in BOTH directions the moment one is not.
25
+ * {@link learningKeyOf} exists so there is one derivation to agree with rather
26
+ * than five to keep in sync.
27
+ *
28
+ * ## Why this file lives under `learnings/` and not under `reconcile/`
29
+ *
30
+ * Dependency direction, measured rather than presumed (2026-08-13, HEAD
31
+ * 5d59e62): `grep -rn "from '../learnings/" scripts/lib/reconcile/` reports 6
32
+ * import edges across 4 files (`eligibility.mjs`, `engine.mjs` ×2,
33
+ * `emitter.mjs` ×2, `renderer.mjs` — three of them pulling THIS module, three
34
+ * pulling `learnings/schema.mjs`), and the reverse grep
35
+ * `from '../reconcile/'` over `scripts/lib/learnings/` reports 0. A primitive
36
+ * shared by both packages therefore belongs on the `learnings/` side; placing
37
+ * it under `reconcile/` would invert an edge and introduce a cycle.
38
+ *
39
+ * Deliberately NOT re-exported from the `scripts/lib/learnings.mjs` barrel —
40
+ * `surface.mjs` and `affinity.mjs` set that precedent: consumers import the
41
+ * leaf directly, so the barrel stays the historical schema/io/filters surface.
42
+ *
43
+ * ## What this module is NOT
44
+ *
45
+ * Not a general-purpose slugifier. Vault note ids, tag segments, and rule
46
+ * filenames have their own slug rules with their own length caps and charset
47
+ * contracts (`vault-mirror/utils.mjs`, `vault-archive.mjs`). Do not route those
48
+ * through here — a shared slugifier across unrelated identity spaces is how a
49
+ * cap added for one consumer silently re-keys another.
50
+ *
51
+ * Pure, stdlib-only, no imports, no clock, no fs.
52
+ */
53
+
54
+ /**
55
+ * Slugify a string into the stable kebab-case token used for learning keys.
56
+ *
57
+ * Lowercases, collapses every run of non-`[a-z0-9]` characters into a single
58
+ * `-`, and trims leading/trailing `-`.
59
+ *
60
+ * Three properties callers depend on:
61
+ *
62
+ * 1. **Total** — never throws. Non-string input is coerced via `String()`,
63
+ * so `null`/`undefined`/numbers yield `"null"`/`"undefined"`/`"12345"`
64
+ * rather than a `TypeError`. Callers pass values typed `unknown` at
65
+ * their trust boundary; a throw there would abort a reconcile run.
66
+ * 2. **Lossy on non-ASCII** — German umlauts and em-dashes in the corpus are
67
+ * collapsed to `-` (`"Größe"` → `"gr-e"`), NOT transliterated. Ugly, and
68
+ * deliberately frozen: every stamped key in `.claude/rules/*.md` and in
69
+ * `.orchestrator/runtime/reconcile-candidates.jsonl` was minted this way.
70
+ * Adding transliteration would re-key the entire corpus.
71
+ * 3. **May return the empty string** — an all-symbol input (`"!!!"`) yields
72
+ * `""`. `reconcile/renderer.mjs::deriveSlug` branches on exactly that to
73
+ * fall back to its hash suffix, so an "always return something non-empty"
74
+ * change here would silently disable that branch.
75
+ *
76
+ * @param {unknown} s Value to slugify; coerced with `String()`.
77
+ * @returns {string} Kebab token; `''` when the input holds no `[a-z0-9]`.
78
+ */
79
+ export function kebab(s) {
80
+ return String(s)
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9]+/g, '-')
83
+ .replace(/^-+|-+$/g, '');
84
+ }
85
+
86
+ /**
87
+ * THE derivation of a learning's logical key: `` `${type}/${kebab(title || subject)}` ``.
88
+ *
89
+ * One rule, four decisions, each of which was a divergence between call sites
90
+ * before this function existed:
91
+ *
92
+ * 1. **The TYPE half is verbatim (trimmed), never kebab'd.** The subject half
93
+ * is prose and must be slugged; the type half is an enum token a reader
94
+ * parses BACK OUT of the key — `backfill-learnings-from-vault.mjs` uses
95
+ * `learning_key.split('/')[0]` as the reconstructed `type` and
96
+ * cross-checks it against the vault note's type. Kebabbing it would make
97
+ * that half lossy, which is precisely the fidelity downgrade the backfill
98
+ * labels `derived:learning-key-slug (original prose not recoverable)` for
99
+ * the subject half. Frontmatter safety does not need it either: the key
100
+ * becomes an unquoted `learning-key:` scalar, and `reconcile/renderer.mjs`
101
+ * already asserts `LEARNING_KEY_RE` (`/^[a-z0-9/-]+$/`) on the whole value
102
+ * before rendering — an unsafe type is REJECTED loudly there rather than
103
+ * silently re-keyed here.
104
+ * 2. **`title` wins over `subject`**, and a whitespace-only `title` falls
105
+ * through to `subject` rather than yielding an empty slug.
106
+ * 3. **`null`, not a throw, for an unkeyable record.** Readers scan corpora
107
+ * that contain shape-foreign lines; an unkeyable record simply does not
108
+ * participate in key resolution. Writers that need a string check for
109
+ * `null` and reject the record with their own auditable reason.
110
+ * 4. **An empty slug is unkeyable, not a key.** `kebab('!!!')` is `''`, and
111
+ * `` `anti-pattern/` `` is not an identity — it is a bucket every
112
+ * all-symbol-subject record of that type would collide in.
113
+ *
114
+ * @param {unknown} record A learning record (or anything; non-records yield `null`).
115
+ * @returns {string|null} `` `${type}/${slug}` ``, or `null` when unkeyable.
116
+ */
117
+ export function learningKeyOf(record) {
118
+ if (record === null || typeof record !== 'object' || Array.isArray(record)) return null;
119
+ const rec = /** @type {Record<string, unknown>} */ (record);
120
+ const type = typeof rec.type === 'string' ? rec.type.trim() : '';
121
+ const titleOrSubject =
122
+ (typeof rec.title === 'string' && rec.title.trim() !== '' ? rec.title : '') ||
123
+ (typeof rec.subject === 'string' && rec.subject.trim() !== '' ? rec.subject : '');
124
+ if (type === '' || titleOrSubject === '') return null;
125
+ const slug = kebab(titleOrSubject);
126
+ if (slug === '') return null;
127
+ return `${type}/${slug}`;
128
+ }
@@ -83,8 +83,13 @@ export const LEARNING_TYPE_REGISTRY = Object.freeze({
83
83
  'fragile-file': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: true }),
84
84
  'effective-sizing': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: false }),
85
85
  'recurring-issue': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: true }),
86
- 'workflow-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
87
- 'proven-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
86
+ // workflow-pattern / proven-pattern: flipped ruleConvertible false->true
87
+ // (issue #900) the real corpus census showed a large volume of live
88
+ // `workflow-pattern`/`proven-pattern` records (post type-alias-normalization,
89
+ // see LEARNING_TYPE_ALIASES below) that carried usable file_paths but were
90
+ // structurally unconvertible before this flip.
91
+ 'workflow-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
92
+ 'proven-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
88
93
  'anti-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
89
94
  'autopilot-effectiveness': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
90
95
  // autonomy-verdict (#683): repo/scope readiness synthesis from autopilot
@@ -106,6 +111,30 @@ export const LEARNING_TYPE_REGISTRY = Object.freeze({
106
111
  'stagnation-class-frequency': Object.freeze({ ttlDays: 60, agentProposable: false, ruleConvertible: true }),
107
112
  });
108
113
 
114
+ /**
115
+ * Free-form producer-dialect type names -> their canonical
116
+ * {@link LEARNING_TYPE_REGISTRY} counterpart (issue #900). The real corpus
117
+ * accumulated learnings stamped with type names that were never registered
118
+ * (`gotcha`, `pattern`, ...) — these are semantically the SAME classes as
119
+ * two registered types, just written with a different literal. Applied by
120
+ * {@link normalizeDialects} on both the read funnel (`normalizeLearning`)
121
+ * and the write/migration funnel (`migrateLegacyLearning`), so every
122
+ * downstream consumer (TTL derivation, `CONVERT_TYPES` / `PROPOSAL_TYPES`
123
+ * membership, reconcile eligibility) sees one canonical type space —
124
+ * mirrors the existing `files` -> `file_paths` dialect-normalization
125
+ * pattern one level up (type name instead of field name).
126
+ *
127
+ * INVARIANT (guarded by a test): no alias KEY may collide with a
128
+ * `LEARNING_TYPE_REGISTRY` key — a collision would silently rewrite an
129
+ * already-canonical type into a different one.
130
+ *
131
+ * @type {Readonly<Record<string, string>>}
132
+ */
133
+ export const LEARNING_TYPE_ALIASES = Object.freeze({
134
+ gotcha: 'anti-pattern',
135
+ pattern: 'proven-pattern',
136
+ });
137
+
109
138
  /**
110
139
  * Per-type TTL policy (in days) for `expires_at` derivation. Derived from
111
140
  * {@link LEARNING_TYPE_REGISTRY} — see parent module documentation for tier
@@ -275,7 +304,10 @@ const _warnedSessionIdConflict = new Set();
275
304
  * session_id/source_session conflict (see below); idempotent; never throws;
276
305
  * non-objects (and arrays) pass through unchanged. Does NOT mutate its input.
277
306
  *
278
- * Dialect rules (Epic #723 B2 — census 2026-07-02):
307
+ * Dialect rules (Epic #723 B2 — census 2026-07-02; type-alias rule added
308
+ * issue #900):
309
+ * - `type` → canonical type — resolved via {@link LEARNING_TYPE_ALIASES}
310
+ * (e.g. `gotcha` → `anti-pattern`, `pattern` → `proven-pattern`).
279
311
  * - `files` → `file_paths` — verbatim value move; an empty array is
280
312
  * preserved as an empty `file_paths`; a canonical
281
313
  * `file_paths` already present wins (legacy `files` dropped).
@@ -310,6 +342,14 @@ export function normalizeDialects(entry, { reserializeTimestamps = true } = {})
310
342
 
311
343
  const out = { ...entry };
312
344
 
345
+ // type alias resolution (#900) — canonicalize free-form producer type names
346
+ // (e.g. `gotcha`, `pattern`) to their LEARNING_TYPE_REGISTRY counterpart so
347
+ // every downstream consumer (TTL derivation, CONVERT_TYPES / PROPOSAL_TYPES
348
+ // membership, reconcile eligibility) sees one canonical type space.
349
+ if (typeof out.type === 'string' && out.type in LEARNING_TYPE_ALIASES) {
350
+ out.type = LEARNING_TYPE_ALIASES[out.type];
351
+ }
352
+
313
353
  // files → file_paths (verbatim value move; canonical wins; empty array kept).
314
354
  if ('files' in out) {
315
355
  if (!('file_paths' in out)) {