@adhdev/mesh-shared 1.0.82-rc.35 → 1.0.82-rc.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/release.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * release — types, limits and pure helpers for release objects
3
+ * (docs/design/2026-11-01-release-objects.md).
4
+ *
5
+ * A release is a mesh-scoped entity that groups MISSIONS (explicit membership) and records
6
+ * deploy facts. It is distinct from the vendor release stamp (a build channel stamp) and
7
+ * from `.adhdev/preview-deploy.json` (the per-checkout deploy record it snapshots).
8
+ * Membership is only ever created by an explicit call; git evaluation and deploy heads
9
+ * produce suggestions, never members. Dependency-free leaf.
10
+ */
11
+
12
+ export const MESH_RELEASE_CHANNELS = ['preview', 'stable'] as const
13
+ export type MeshReleaseChannel = typeof MESH_RELEASE_CHANNELS[number]
14
+ export function isMeshReleaseChannel(value: unknown): value is MeshReleaseChannel {
15
+ return typeof value === 'string' && (MESH_RELEASE_CHANNELS as readonly string[]).includes(value)
16
+ }
17
+
18
+ export const MESH_RELEASE_STATUSES = ['planned', 'frozen', 'deployed', 'verified', 'superseded', 'cancelled'] as const
19
+ export type ReleaseStatus = typeof MESH_RELEASE_STATUSES[number]
20
+ export function isReleaseStatus(value: unknown): value is ReleaseStatus {
21
+ return typeof value === 'string' && (MESH_RELEASE_STATUSES as readonly string[]).includes(value)
22
+ }
23
+
24
+ export const RELEASE_MEMBER_STATES = ['member', 'moved_out', 'removed'] as const
25
+ export type ReleaseMemberState = typeof RELEASE_MEMBER_STATES[number]
26
+
27
+ /** Who may write release membership. The daemon never adds a member on its own. */
28
+ export const RELEASE_ACTORS = ['coordinator', 'owner', 'assistant', 'script'] as const
29
+ export type ReleaseActor = typeof RELEASE_ACTORS[number]
30
+
31
+ export const RELEASE_EVENT_TYPES = [
32
+ 'created', 'status_changed', 'scope_added', 'scope_moved', 'scope_removed',
33
+ 'deploy_recorded', 'changelog_drafted', 'changelog_applied',
34
+ ] as const
35
+ export type ReleaseEventType = typeof RELEASE_EVENT_TYPES[number]
36
+
37
+ /** Mission-side events recorded when membership changes (so the mission history shows it). */
38
+ export const MISSION_RELEASE_EVENT_TYPES = ['release_attached', 'release_moved', 'release_detached'] as const
39
+
40
+ export const RELEASE_LIMITS = {
41
+ name: 80,
42
+ note: 500,
43
+ /** `mesh_missions.release_note`: one user/operator-visible sentence. */
44
+ releaseNote: 400,
45
+ missionsPerCall: 100,
46
+ changelogEntries: 300,
47
+ } as const
48
+
49
+ /** Deploy targets snapshot (a copy of `.adhdev/preview-deploy.json` `targets`). */
50
+ export interface ReleaseDeployTarget {
51
+ commit?: string
52
+ deployedAt?: string
53
+ previewVersion?: string
54
+ [key: string]: unknown
55
+ }
56
+ export type ReleaseTargets = Partial<Record<'npm' | 'server' | 'web', ReleaseDeployTarget>> & Record<string, ReleaseDeployTarget | undefined>
57
+
58
+ export const CHANGELOG_SECTIONS = ['Added', 'Fixed', 'Changed'] as const
59
+ export type ChangelogSection = typeof CHANGELOG_SECTIONS[number]
60
+
61
+ export interface ChangelogDraftEntry {
62
+ missionId: string
63
+ /** `<!-- m:<id8> -->` — the idempotency marker `--apply` writes after the line. */
64
+ marker: string
65
+ section: ChangelogSection
66
+ /** The sentence that would be written to CHANGELOG.md. */
67
+ text: string
68
+ source: 'release_note' | 'summary'
69
+ /** True when `text` is not user-facing wording yet (carries the needs-wording marker). */
70
+ needsWording?: boolean
71
+ /** Evidence kept in the draft JSON only; never written to CHANGELOG.md. */
72
+ evidence?: { gitState?: string; outcome?: string; shas?: string[] }
73
+ /** Non-blocking lint warnings (internal ids / wave numbers / forbidden phrases). */
74
+ lint?: string[]
75
+ }
76
+
77
+ export interface ChangelogDraftExcluded {
78
+ missionId: string
79
+ reason: string
80
+ }
81
+
82
+ export interface ChangelogDraft {
83
+ generatedAt: string
84
+ entries: ChangelogDraftEntry[]
85
+ /** Member missions without a `release_note` (target 0 at deploy time). */
86
+ needsNote: string[]
87
+ excluded: ChangelogDraftExcluded[]
88
+ /** Member missions whose refs are not confirmed on the default branch (`not_landed` / `unknown`). */
89
+ warnings: Array<{ missionId: string; gitState: string; message: string }>
90
+ appliedAt?: string
91
+ }
92
+
93
+ export const NEEDS_WORDING_MARKER = '[needs user-facing wording]'
94
+
95
+ /** Marker that makes `--apply` idempotent: one per mission entry inside CHANGELOG.md. */
96
+ export function changelogMissionMarker(missionId: string): string {
97
+ return `<!-- m:${missionId.slice(0, 8)} -->`
98
+ }
99
+
100
+ /** Default section for an entry; `[Fixed]`/`[Added]`/`[Changed]` prefixes on the note pin it. */
101
+ export function classifyChangelogSection(text: string, hint?: string): { section: ChangelogSection; text: string } {
102
+ const m = /^\s*\[(added|fixed|changed)\]\s*/i.exec(text)
103
+ if (m) {
104
+ const key = m[1].toLowerCase()
105
+ return { section: key === 'added' ? 'Added' : key === 'fixed' ? 'Fixed' : 'Changed', text: text.slice(m[0].length).trim() }
106
+ }
107
+ const probe = `${hint ?? ''} ${text}`.toLowerCase()
108
+ if (/\b(fix|fixes|fixed|bug|regression|crash|error)\b|오류|결함|버그|수정/.test(probe)) return { section: 'Fixed', text }
109
+ if (/\b(add|adds|added|new|introduc\w*|support\w*)\b|신규|추가/.test(probe)) return { section: 'Added', text }
110
+ return { section: 'Changed', text }
111
+ }
112
+
113
+ /** Phrases that must not reach a public CHANGELOG (internal waves, rc numbering, ids). Warning-only. */
114
+ export function lintChangelogText(text: string): string[] {
115
+ const out: string[] = []
116
+ if (/\bwave[\s-]*\d+/i.test(text) || /웨이브\s*\d+/.test(text)) out.push('mentions an internal wave number')
117
+ if (/\brc[.\s-]?\d+\b/i.test(text)) out.push('mentions an internal rc number')
118
+ if (/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i.test(text) || /\bmi_[0-9a-f]{12}\b/.test(text) || /\brel_[0-9a-f]{12}\b/.test(text)) out.push('contains an internal id')
119
+ if (/\bmission\b/i.test(text)) out.push('mentions "mission" (internal vocabulary)')
120
+ return out
121
+ }
@@ -101,6 +101,13 @@ export interface ModelSelection {
101
101
  /** Last value the provider itself reported (statusline, native history). */
102
102
  observed?: string
103
103
  observedAt?: number
104
+ /**
105
+ * The provider SILENTLY substituted another model for the requested one
106
+ * (antigravity: `model X is not recognized … Using "Y" instead`). `observed`
107
+ * then holds the model actually running. P2P/launch-record only — the
108
+ * server path never carries it (see the content boundary above).
109
+ */
110
+ fallback?: boolean
104
111
  /** Every value change in order. The last entry is the value in force. */
105
112
  history: ModelSelectionHistoryEntry[]
106
113
  }
@@ -206,6 +213,7 @@ function parseModelSelection(raw: unknown): ModelSelection | undefined {
206
213
  const current = readString(record.current)
207
214
  const observed = readString(record.observed)
208
215
  const observedAt = readFiniteNumber(record.observedAt)
216
+ const fallback = record.fallback === true
209
217
  return {
210
218
  source: isModelAxisSource(record.source) ? record.source : 'unspecified',
211
219
  ...(requested ? { requested } : {}),
@@ -214,6 +222,7 @@ function parseModelSelection(raw: unknown): ModelSelection | undefined {
214
222
  ...(current ? { current } : {}),
215
223
  ...(observed ? { observed } : {}),
216
224
  ...(observedAt !== undefined ? { observedAt } : {}),
225
+ ...(fallback ? { fallback } : {}),
217
226
  history: history.slice(-MODEL_SELECTION_HISTORY_LIMIT),
218
227
  }
219
228
  }
@@ -94,6 +94,8 @@ export interface MissionUpsertRequest {
94
94
  */
95
95
  structured?: boolean
96
96
  summary?: string
97
+ /** One user/operator-visible sentence for the release CHANGELOG draft (design 2026-11-01-release-objects L5). `''` clears. */
98
+ releaseNote?: string
97
99
  outcome?: string
98
100
  outcomeReason?: string
99
101
  /** Mission id open items move to when this call closes the mission (default: the carry-over mission). */
@@ -114,6 +116,7 @@ export interface MeshMissionOpenItemCountsWire {
114
116
  /** Structured-missions fields (only with `structured: true` on the request). */
115
117
  export interface MeshMissionStructuredWire {
116
118
  summary?: string
119
+ releaseNote?: string
117
120
  outcome?: string
118
121
  outcomeReason?: string
119
122
  version?: number
@@ -137,7 +140,7 @@ export interface MissionUpsertResponse {
137
140
  result?: Record<string, unknown>
138
141
  }
139
142
 
140
- const STRUCTURED_MISSION_KEYS = ['summary', 'outcome', 'outcomeReason', 'version', 'carryOver', 'openItems'] as const
143
+ const STRUCTURED_MISSION_KEYS = ['summary', 'releaseNote', 'outcome', 'outcomeReason', 'version', 'carryOver', 'openItems'] as const
141
144
 
142
145
  function isOpenItemCountsWire(value: unknown): value is MeshMissionOpenItemCountsWire {
143
146
  if (!isRecord(value) || !hasOnlyKeys(value, ['remaining', 'live_check', 'decision', 'evidence'])) return false
@@ -145,7 +148,7 @@ function isOpenItemCountsWire(value: unknown): value is MeshMissionOpenItemCount
145
148
  }
146
149
 
147
150
  function isStructuredMissionFields(value: Record<string, unknown>): boolean {
148
- for (const k of ['summary', 'outcome', 'outcomeReason'] as const) {
151
+ for (const k of ['summary', 'releaseNote', 'outcome', 'outcomeReason'] as const) {
149
152
  if (value[k] !== undefined && typeof value[k] !== 'string') return false
150
153
  }
151
154
  if (value.version !== undefined && !isNonNegativeInt(value.version)) return false
@@ -165,7 +168,7 @@ function isMeshMissionRecordWire(value: unknown): value is MeshMissionRecordWire
165
168
  }
166
169
 
167
170
  function isMissionUpsertRequest(value: unknown): value is MissionUpsertRequest {
168
- if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'id', 'title', 'goal', 'status', 'brief', 'structured', 'summary', 'outcome', 'outcomeReason', 'carryOverTo', 'toCarryOver', 'itemsAdd', 'itemsUpdate'])) return false
171
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'id', 'title', 'goal', 'status', 'brief', 'structured', 'summary', 'releaseNote', 'outcome', 'outcomeReason', 'carryOverTo', 'toCarryOver', 'itemsAdd', 'itemsUpdate'])) return false
169
172
  if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
170
173
  if (!isEvidenceIdentifier(value.meshId)) return false
171
174
  if (value.id !== undefined && !isEvidenceIdentifier(value.id)) return false
@@ -174,7 +177,7 @@ function isMissionUpsertRequest(value: unknown): value is MissionUpsertRequest {
174
177
  if (value.title.trim().length === 0 && value.toCarryOver !== true && value.id === undefined) return false
175
178
  if (value.structured !== undefined && typeof value.structured !== 'boolean') return false
176
179
  if (value.toCarryOver !== undefined && typeof value.toCarryOver !== 'boolean') return false
177
- for (const k of ['summary', 'outcome', 'outcomeReason'] as const) {
180
+ for (const k of ['summary', 'releaseNote', 'outcome', 'outcomeReason'] as const) {
178
181
  if (value[k] !== undefined && typeof value[k] !== 'string') return false
179
182
  }
180
183
  if (value.carryOverTo !== undefined && !isEvidenceIdentifier(value.carryOverTo)) return false
@@ -324,6 +327,7 @@ export interface MissionListSummaryVerboseWire {
324
327
  closeCandidateEmittedAt?: string | null
325
328
  /** Structured lists only. */
326
329
  summary?: string
330
+ releaseNote?: string
327
331
  outcome?: string
328
332
  outcomeReason?: string
329
333
  version?: number
@@ -351,6 +355,7 @@ export interface MissionListSummarySlimWire {
351
355
  closeCandidateEmittedAt?: string | null
352
356
  /** Structured lists only. */
353
357
  summary?: string
358
+ releaseNote?: string
354
359
  outcome?: string
355
360
  outcomeReason?: string
356
361
  version?: number