release-skill 0.1.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 (125) hide show
  1. package/.agents/plugins/marketplace.json +23 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +26 -0
  5. package/CHANGELOG.md +68 -0
  6. package/CODE_OF_CONDUCT.md +76 -0
  7. package/CONTRIBUTING.md +49 -0
  8. package/INSTALL.md +182 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +25 -0
  11. package/README.md +501 -0
  12. package/README.zh-CN.md +463 -0
  13. package/SECURITY.md +48 -0
  14. package/adapters/claude/.claude-plugin/marketplace.json +16 -0
  15. package/adapters/claude/.claude-plugin/plugin.json +10 -0
  16. package/adapters/claude/skills/release-assess/SKILL.md +52 -0
  17. package/adapters/claude/skills/release-help/SKILL.md +60 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
  19. package/adapters/claude/skills/release-publish/SKILL.md +55 -0
  20. package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
  21. package/adapters/claude/skills/release-verify/SKILL.md +70 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +26 -0
  23. package/adapters/codex/skills/release-assess/SKILL.md +52 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +60 -0
  25. package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
  26. package/adapters/codex/skills/release-publish/SKILL.md +55 -0
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
  28. package/adapters/codex/skills/release-verify/SKILL.md +70 -0
  29. package/bin/release-skill.mjs +743 -0
  30. package/native/safe-write/binding.gyp +40 -0
  31. package/native/safe-write/prebuilds.json +4 -0
  32. package/native/safe-write/src/safe_write.cc +2023 -0
  33. package/package.json +75 -0
  34. package/references/.render-manifest.json +33 -0
  35. package/references/00-target-state.md +124 -0
  36. package/references/01-state-machine.md +155 -0
  37. package/references/02-project-config.md +217 -0
  38. package/references/03-readme-quality.md +136 -0
  39. package/references/04-supply-chain.md +147 -0
  40. package/references/05-evidence-and-errors.md +164 -0
  41. package/references/06-adapter-contract.md +178 -0
  42. package/schemas/.render-manifest.json +37 -0
  43. package/schemas/approval-record.schema.json +115 -0
  44. package/schemas/artifact-lock.schema.json +111 -0
  45. package/schemas/artifact-plan.schema.json +52 -0
  46. package/schemas/artifact-policy.schema.json +76 -0
  47. package/schemas/evidence-event.schema.json +89 -0
  48. package/schemas/release-plan.schema.json +369 -0
  49. package/schemas/release-project.schema.json +359 -0
  50. package/schemas/release-run.schema.json +195 -0
  51. package/skills/release-assess/SKILL.md +52 -0
  52. package/skills/release-help/SKILL.md +60 -0
  53. package/skills/release-prepare/SKILL.md +71 -0
  54. package/skills/release-publish/SKILL.md +55 -0
  55. package/skills/release-reconcile/SKILL.md +73 -0
  56. package/skills/release-verify/SKILL.md +70 -0
  57. package/skills-src/release-assess/SKILL.md +52 -0
  58. package/skills-src/release-help/SKILL.md +60 -0
  59. package/skills-src/release-prepare/SKILL.md +71 -0
  60. package/skills-src/release-publish/SKILL.md +55 -0
  61. package/skills-src/release-reconcile/SKILL.md +73 -0
  62. package/skills-src/release-verify/SKILL.md +70 -0
  63. package/src/adapters/contract.mjs +214 -0
  64. package/src/adapters/git-github.mjs +214 -0
  65. package/src/adapters/npm.mjs +947 -0
  66. package/src/adapters/plugin-marketplace.mjs +1365 -0
  67. package/src/adapters/push-snapshot.mjs +216 -0
  68. package/src/artifacts/adoption.mjs +743 -0
  69. package/src/artifacts/artifact-plan.mjs +162 -0
  70. package/src/artifacts/entry.mjs +240 -0
  71. package/src/artifacts/git-authority.mjs +637 -0
  72. package/src/artifacts/graph.mjs +189 -0
  73. package/src/artifacts/inspect.mjs +520 -0
  74. package/src/artifacts/inventory.mjs +192 -0
  75. package/src/artifacts/merge/binary.mjs +77 -0
  76. package/src/artifacts/merge/entry-merge.mjs +228 -0
  77. package/src/artifacts/merge/json.mjs +641 -0
  78. package/src/artifacts/merge/markdown.mjs +246 -0
  79. package/src/artifacts/merge/regions.mjs +156 -0
  80. package/src/artifacts/merge/text.mjs +432 -0
  81. package/src/artifacts/merge/tree.mjs +202 -0
  82. package/src/artifacts/merge/yaml.mjs +669 -0
  83. package/src/artifacts/path-key.mjs +94 -0
  84. package/src/artifacts/policy.mjs +319 -0
  85. package/src/artifacts/producer-registry.mjs +439 -0
  86. package/src/artifacts/project-lock.mjs +732 -0
  87. package/src/artifacts/resolution.mjs +658 -0
  88. package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
  89. package/src/artifacts/safe-fs.mjs +72 -0
  90. package/src/artifacts/state.mjs +495 -0
  91. package/src/artifacts/transaction-journal.mjs +983 -0
  92. package/src/artifacts/transaction.mjs +1361 -0
  93. package/src/commands/approve.mjs +280 -0
  94. package/src/commands/artifacts.mjs +627 -0
  95. package/src/commands/assess.mjs +838 -0
  96. package/src/commands/prepare.mjs +1377 -0
  97. package/src/commands/publish.mjs +883 -0
  98. package/src/commands/reconcile.mjs +1255 -0
  99. package/src/commands/verify.mjs +915 -0
  100. package/src/core/approval.mjs +332 -0
  101. package/src/core/baseline.mjs +272 -0
  102. package/src/core/blackbox-hard-gates.mjs +142 -0
  103. package/src/core/config.mjs +448 -0
  104. package/src/core/digest.mjs +90 -0
  105. package/src/core/errors.mjs +113 -0
  106. package/src/core/evidence.mjs +167 -0
  107. package/src/core/hooks.mjs +241 -0
  108. package/src/core/node-version.mjs +64 -0
  109. package/src/core/plan.mjs +735 -0
  110. package/src/core/previous-public-baseline.mjs +204 -0
  111. package/src/core/run.mjs +681 -0
  112. package/src/core/state-machine.mjs +76 -0
  113. package/src/core/version-consistency.mjs +111 -0
  114. package/src/producers/build-adapters.mjs +231 -0
  115. package/src/producers/render-public-assets.mjs +152 -0
  116. package/src/producers/sync-skills.mjs +96 -0
  117. package/src/readme/contract.mjs +297 -0
  118. package/src/readme/examples.mjs +288 -0
  119. package/src/readme/parity.mjs +122 -0
  120. package/src/snapshot/export.mjs +99 -0
  121. package/src/snapshot/frozen.mjs +401 -0
  122. package/src/snapshot/manifest.mjs +207 -0
  123. package/src/snapshot/public-map.mjs +1459 -0
  124. package/src/snapshot/public-path.mjs +110 -0
  125. package/src/snapshot/scan.mjs +419 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Markdown format-preserving three-way merge.
3
+ *
4
+ * Splits the file into frontmatter, managed regions (declared by start/end
5
+ * markers), and unmanaged body ranges. Unmanaged ranges are byte-preserved
6
+ * from the current side. Managed regions are merged via text diff3 on the
7
+ * region content. Frontmatter is merged via text diff3 on the YAML block.
8
+ *
9
+ * No full-file parse or stringify is ever performed — all edits are
10
+ * localised byte-range replacements.
11
+ *
12
+ * @module artifacts/merge/markdown
13
+ */
14
+
15
+ import { parseManagedRegions } from './regions.mjs';
16
+ import { mergeText } from './text.mjs';
17
+
18
+ /**
19
+ * Detect YAML frontmatter in a Markdown buffer.
20
+ *
21
+ * Frontmatter starts with `---\n` at offset 0 and ends at the next `---\n`.
22
+ *
23
+ * @param {Buffer} bytes
24
+ * @returns {{ endOffset: number }|null} End offset (exclusive) of the frontmatter
25
+ * block including the closing `---\n`, or null if no frontmatter.
26
+ */
27
+ function detectFrontmatter(bytes) {
28
+ const text = bytes.toString('utf8');
29
+ if (!text.startsWith('---\n') && !text.startsWith('---\r\n')) return null;
30
+
31
+ // Find the closing ---
32
+ const afterFirstDelimiter = text.startsWith('---\r\n') ? 5 : 4;
33
+ const rest = text.substring(afterFirstDelimiter);
34
+ const closeIdx = rest.indexOf('\n---\n');
35
+ if (closeIdx < 0) return null;
36
+
37
+ // end offset = afterFirstDelimiter + closeIdx + length of "\n---\n"
38
+ return { endOffset: afterFirstDelimiter + closeIdx + 5 };
39
+ }
40
+
41
+ /**
42
+ * Merge two Markdown modifications of the same base content.
43
+ *
44
+ * @param {object} options
45
+ * @param {Buffer} options.base - Base Markdown content.
46
+ * @param {Buffer} options.current - Current (human) Markdown content.
47
+ * @param {Buffer} options.generated - Generated (producer) Markdown content.
48
+ * @param {Array<{ id: string, start: string, end: string }>} [options.managedRegions]
49
+ * Managed-region declarations. Each region is delimited by start/end markers.
50
+ * @returns {{ status: 'MERGEABLE'|'CONFLICT'|'STRUCTURE_INVALID', bytes?: Buffer, conflicts: object[] }}
51
+ */
52
+ export function mergeMarkdown({ base, current, generated, managedRegions = [] } = {}) {
53
+ // Null/missing inputs → CONFLICT
54
+ if (!base || !current || !generated) {
55
+ return Object.freeze({
56
+ status: 'CONFLICT',
57
+ bytes: undefined,
58
+ conflicts: Object.freeze([{ reason: 'missing input' }]),
59
+ });
60
+ }
61
+
62
+ // Parse managed regions from the CURRENT side (authoritative layout)
63
+ let currentRanges;
64
+ try {
65
+ currentRanges = parseManagedRegions(current, managedRegions);
66
+ } catch (err) {
67
+ if (err.code === 'STRUCTURE_INVALID') {
68
+ return Object.freeze({
69
+ status: 'STRUCTURE_INVALID',
70
+ bytes: undefined,
71
+ conflicts: Object.freeze([{ reason: err.message }]),
72
+ });
73
+ }
74
+ throw err;
75
+ }
76
+
77
+ // Also parse from base and generated to verify consistency
78
+ let baseRanges;
79
+ try {
80
+ baseRanges = parseManagedRegions(base, managedRegions);
81
+ } catch (err) {
82
+ if (err.code === 'STRUCTURE_INVALID') {
83
+ return Object.freeze({
84
+ status: 'STRUCTURE_INVALID',
85
+ bytes: undefined,
86
+ conflicts: Object.freeze([{ reason: `base: ${err.message}` }]),
87
+ });
88
+ }
89
+ throw err;
90
+ }
91
+
92
+ let generatedRanges;
93
+ try {
94
+ generatedRanges = parseManagedRegions(generated, managedRegions);
95
+ } catch (err) {
96
+ if (err.code === 'STRUCTURE_INVALID') {
97
+ return Object.freeze({
98
+ status: 'STRUCTURE_INVALID',
99
+ bytes: undefined,
100
+ conflicts: Object.freeze([{ reason: `generated: ${err.message}` }]),
101
+ });
102
+ }
103
+ throw err;
104
+ }
105
+
106
+ // Detect frontmatter on all three sides
107
+ const baseFm = detectFrontmatter(base);
108
+ const currentFm = detectFrontmatter(current);
109
+ const genFm = detectFrontmatter(generated);
110
+
111
+ // Build output by walking unmanaged and managed ranges in order
112
+ const output = [];
113
+ let cursor = 0;
114
+
115
+ // Process frontmatter first (if present on any side).
116
+ // Three-way frontmatter rules for all 8 presence combinations:
117
+ // base current generated → action
118
+ // 0 0 0 → no FM
119
+ // 0 0 1 → include generated FM (generated-only addition)
120
+ // 0 1 0 → preserve current FM (human addition)
121
+ // 0 1 1 → preserve current FM (human overrides)
122
+ // 1 0 0 → accept removal
123
+ // 1 0 1 → if changed → CONFLICT; else accept removal
124
+ // 1 1 0 → preserve current FM (generated removed, human kept)
125
+ // 1 1 1 → three-way merge
126
+ if (currentFm || baseFm || genFm) {
127
+ const baseFmEnd = baseFm?.endOffset ?? 0;
128
+ const currentFmEnd = currentFm?.endOffset ?? 0;
129
+ const genFmEnd = genFm?.endOffset ?? 0;
130
+
131
+ if (baseFm && currentFm && genFm) {
132
+ // All three have frontmatter — three-way merge
133
+ const baseFmBytes = base.subarray(0, baseFmEnd);
134
+ const currentFmBytes = current.subarray(0, currentFmEnd);
135
+ const genFmBytes = generated.subarray(0, genFmEnd);
136
+
137
+ const fmResult = mergeText({
138
+ base: baseFmBytes,
139
+ current: currentFmBytes,
140
+ generated: genFmBytes,
141
+ });
142
+
143
+ if (fmResult.status === 'CONFLICT') {
144
+ return Object.freeze({
145
+ status: 'CONFLICT',
146
+ bytes: undefined,
147
+ conflicts: Object.freeze(fmResult.conflicts.map((c) =>
148
+ Object.freeze({ ...c, regionId: 'frontmatter' }),
149
+ )),
150
+ });
151
+ }
152
+
153
+ output.push(fmResult.bytes);
154
+ cursor = currentFmEnd;
155
+ } else if (!baseFm && !currentFm && genFm) {
156
+ // Generated-only new frontmatter — include it
157
+ output.push(generated.subarray(0, genFmEnd));
158
+ cursor = 0; // no frontmatter consumed from current
159
+ } else if (currentFm) {
160
+ // Current has frontmatter — preserve it (covers 0/1/0, 0/1/1, 1/1/0)
161
+ output.push(current.subarray(0, currentFmEnd));
162
+ cursor = currentFmEnd;
163
+ } else if (baseFm && genFm) {
164
+ // Base and generated have frontmatter, current removed it (1/0/1)
165
+ const baseFmContent = base.subarray(0, baseFmEnd);
166
+ const genFmContent = generated.subarray(0, genFmEnd);
167
+ if (!baseFmContent.equals(genFmContent)) {
168
+ // Generated changed frontmatter but current removed it → conflict
169
+ return Object.freeze({
170
+ status: 'CONFLICT',
171
+ bytes: undefined,
172
+ conflicts: Object.freeze([{
173
+ reason: 'frontmatter removed by current but modified by generated',
174
+ regionId: 'frontmatter',
175
+ }]),
176
+ });
177
+ }
178
+ // Generated didn't change frontmatter — accept current's removal (1/0/0)
179
+ }
180
+ }
181
+
182
+ // Sort regions by start offset for ordered processing
183
+ const sortedRegions = [...currentRanges.entries()]
184
+ .sort(([, a], [, b]) => a.start - b.start);
185
+
186
+ for (const [regionId, currentRange] of sortedRegions) {
187
+ // Emit unmanaged bytes before this region (from current)
188
+ if (cursor < currentRange.start) {
189
+ output.push(current.subarray(cursor, currentRange.start));
190
+ }
191
+
192
+ // Get the corresponding range in base and generated
193
+ const baseRange = baseRanges.get(regionId);
194
+ const genRange = generatedRanges.get(regionId);
195
+
196
+ if (!baseRange || !genRange) {
197
+ // Region missing in base or generated — structural mismatch
198
+ return Object.freeze({
199
+ status: 'STRUCTURE_INVALID',
200
+ bytes: undefined,
201
+ conflicts: Object.freeze([{
202
+ reason: `managed region '${regionId}' has inconsistent marker positions across sides`,
203
+ }]),
204
+ });
205
+ }
206
+
207
+ // Extract region content from each side (between markers, exclusive)
208
+ const baseContent = base.subarray(baseRange.start, baseRange.end);
209
+ const currentContent = current.subarray(currentRange.start, currentRange.end);
210
+ const generatedContent = generated.subarray(genRange.start, genRange.end);
211
+
212
+ // Text merge the region content
213
+ const regionResult = mergeText({
214
+ base: baseContent,
215
+ current: currentContent,
216
+ generated: generatedContent,
217
+ });
218
+
219
+ if (regionResult.status === 'CONFLICT') {
220
+ return Object.freeze({
221
+ status: 'CONFLICT',
222
+ bytes: undefined,
223
+ conflicts: Object.freeze(regionResult.conflicts.map((c) =>
224
+ Object.freeze({ ...c, regionId }),
225
+ )),
226
+ });
227
+ }
228
+
229
+ // Emit merged region bytes
230
+ output.push(regionResult.bytes);
231
+ cursor = currentRange.end;
232
+ }
233
+
234
+ // Emit any trailing unmanaged bytes after the last region
235
+ if (cursor < current.length) {
236
+ output.push(current.subarray(cursor));
237
+ }
238
+
239
+ const bytes = Buffer.concat(output);
240
+
241
+ return Object.freeze({
242
+ status: 'MERGEABLE',
243
+ bytes,
244
+ conflicts: Object.freeze([]),
245
+ });
246
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Managed region parser for structured merge drivers.
3
+ *
4
+ * Parses managed-region declarations (start/end marker pairs) from raw
5
+ * byte content and returns a projection map of region id → byte range.
6
+ *
7
+ * Enforces:
8
+ * - No duplicate start or end markers for the same region id.
9
+ * - No nested or overlapping regions.
10
+ * - Every declared region must have both start and end markers present.
11
+ *
12
+ * All operations are byte-level; no full-file parse or stringify.
13
+ *
14
+ * @module artifacts/merge/regions
15
+ */
16
+
17
+ import { ReleaseError, STRUCTURE_INVALID } from '../../core/errors.mjs';
18
+
19
+ /**
20
+ * @typedef {{ start: number, end: number }} ByteRange
21
+ * start is inclusive byte offset of the start marker; end is the exclusive
22
+ * byte offset past the end marker (i.e., end marker last byte + 1).
23
+ */
24
+
25
+ /**
26
+ * Parse managed-region declarations from raw bytes.
27
+ *
28
+ * Returns a Map of region id → { start, end } byte ranges.
29
+ *
30
+ * @param {Buffer} bytes - Raw file content.
31
+ * @param {Array<{ id: string, start: string, end: string }>} declarations
32
+ * @returns {Map<string, ByteRange>}
33
+ * @throws {ReleaseError} with code STRUCTURE_INVALID on duplicate/nested/missing markers.
34
+ */
35
+ export function parseManagedRegions(bytes, declarations) {
36
+ const result = new Map();
37
+
38
+ // Reject empty marker strings upfront to prevent infinite loops
39
+ for (const decl of declarations) {
40
+ if (!decl.start || !decl.end) {
41
+ throw new ReleaseError(
42
+ STRUCTURE_INVALID,
43
+ `managed region '${decl.id}' has empty start or end marker`,
44
+ { regionId: decl.id },
45
+ );
46
+ }
47
+ }
48
+
49
+ // Reject duplicate declaration ids
50
+ const seenIds = new Set();
51
+ for (const decl of declarations) {
52
+ if (seenIds.has(decl.id)) {
53
+ throw new ReleaseError(
54
+ STRUCTURE_INVALID,
55
+ `duplicate declaration id '${decl.id}' in managed region declarations`,
56
+ { regionId: decl.id },
57
+ );
58
+ }
59
+ seenIds.add(decl.id);
60
+ }
61
+
62
+ /** @type {Array<{ id: string, kind: 'start'|'end', offset: number }>} */
63
+ const markers = [];
64
+
65
+ // Collect all marker occurrences
66
+ for (const decl of declarations) {
67
+ let offset = 0;
68
+ while (offset <= bytes.length) {
69
+ const idx = bytes.indexOf(decl.start, offset, 'utf8');
70
+ if (idx < 0) break;
71
+ markers.push({ id: decl.id, kind: 'start', offset: idx });
72
+ offset = idx + Buffer.byteLength(decl.start, 'utf8');
73
+ // Guard: if marker is empty this would be an infinite loop (handled above)
74
+ }
75
+ offset = 0;
76
+ while (offset <= bytes.length) {
77
+ const idx = bytes.indexOf(decl.end, offset, 'utf8');
78
+ if (idx < 0) break;
79
+ markers.push({ id: decl.id, kind: 'end', offset: idx });
80
+ offset = idx + Buffer.byteLength(decl.end, 'utf8');
81
+ }
82
+ }
83
+
84
+ // Reject declared regions whose markers are completely absent from content
85
+ for (const decl of declarations) {
86
+ const hasStart = markers.some((m) => m.id === decl.id && m.kind === 'start');
87
+ const hasEnd = markers.some((m) => m.id === decl.id && m.kind === 'end');
88
+ if (!hasStart && !hasEnd) {
89
+ throw new ReleaseError(
90
+ STRUCTURE_INVALID,
91
+ `managed region '${decl.id}' markers not found in content`,
92
+ { regionId: decl.id },
93
+ );
94
+ }
95
+ }
96
+
97
+ // Sort by offset, then 'start' before 'end' at same offset
98
+ markers.sort((a, b) => a.offset - b.offset || (a.kind === 'start' ? -1 : 1));
99
+
100
+ // Validate and extract ranges
101
+ /** @type {Map<string, { startOffset: number }>} */
102
+ const openRegions = new Map();
103
+
104
+ for (const marker of markers) {
105
+ if (marker.kind === 'start') {
106
+ if (openRegions.has(marker.id)) {
107
+ throw new ReleaseError(
108
+ STRUCTURE_INVALID,
109
+ `duplicate start marker for managed region '${marker.id}'`,
110
+ { regionId: marker.id, offset: marker.offset },
111
+ );
112
+ }
113
+ // Check for nesting: any currently-open region with a different id
114
+ // whose start is after this offset indicates overlap
115
+ for (const [openId, openInfo] of openRegions) {
116
+ if (openId !== marker.id && openInfo.startOffset < marker.offset) {
117
+ // There's an open region that started before this one — overlapping
118
+ throw new ReleaseError(
119
+ STRUCTURE_INVALID,
120
+ `managed region '${marker.id}' overlaps with open region '${openId}'`,
121
+ { regionId: marker.id, overlappingWith: openId },
122
+ );
123
+ }
124
+ }
125
+ openRegions.set(marker.id, { startOffset: marker.offset });
126
+ } else {
127
+ // end marker
128
+ if (!openRegions.has(marker.id)) {
129
+ throw new ReleaseError(
130
+ STRUCTURE_INVALID,
131
+ `end marker for managed region '${marker.id}' found without matching start`,
132
+ { regionId: marker.id, offset: marker.offset },
133
+ );
134
+ }
135
+ const openInfo = openRegions.get(marker.id);
136
+ const decl = declarations.find((d) => d.id === marker.id);
137
+ const endMarkerLen = Buffer.byteLength(decl.end, 'utf8');
138
+ result.set(marker.id, {
139
+ start: openInfo.startOffset,
140
+ end: marker.offset + endMarkerLen,
141
+ });
142
+ openRegions.delete(marker.id);
143
+ }
144
+ }
145
+
146
+ // Any remaining open regions are missing their end markers
147
+ for (const [id] of openRegions) {
148
+ throw new ReleaseError(
149
+ STRUCTURE_INVALID,
150
+ `managed region '${id}' has start marker but no end marker`,
151
+ { regionId: id },
152
+ );
153
+ }
154
+
155
+ return result;
156
+ }