@eddacraft/anvil-core 0.1.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 (215) hide show
  1. package/LICENSE +14 -0
  2. package/dist/antipattern/index.d.ts +11 -0
  3. package/dist/antipattern/index.d.ts.map +1 -0
  4. package/dist/antipattern/index.js +31 -0
  5. package/dist/antipattern/patterns-css.d.ts +17 -0
  6. package/dist/antipattern/patterns-css.d.ts.map +1 -0
  7. package/dist/antipattern/patterns-css.js +72 -0
  8. package/dist/antipattern/patterns-html.d.ts +21 -0
  9. package/dist/antipattern/patterns-html.d.ts.map +1 -0
  10. package/dist/antipattern/patterns-html.js +139 -0
  11. package/dist/antipattern/patterns.d.ts +72 -0
  12. package/dist/antipattern/patterns.d.ts.map +1 -0
  13. package/dist/antipattern/patterns.js +301 -0
  14. package/dist/antipattern/scanner.d.ts +32 -0
  15. package/dist/antipattern/scanner.d.ts.map +1 -0
  16. package/dist/antipattern/scanner.js +89 -0
  17. package/dist/antipattern/types.d.ts +318 -0
  18. package/dist/antipattern/types.d.ts.map +1 -0
  19. package/dist/antipattern/types.js +278 -0
  20. package/dist/architecture/analyzer.d.ts +123 -0
  21. package/dist/architecture/analyzer.d.ts.map +1 -0
  22. package/dist/architecture/analyzer.js +321 -0
  23. package/dist/architecture/baseline.d.ts +112 -0
  24. package/dist/architecture/baseline.d.ts.map +1 -0
  25. package/dist/architecture/baseline.js +245 -0
  26. package/dist/architecture/compiler.d.ts +24 -0
  27. package/dist/architecture/compiler.d.ts.map +1 -0
  28. package/dist/architecture/compiler.js +57 -0
  29. package/dist/architecture/context.d.ts +129 -0
  30. package/dist/architecture/context.d.ts.map +1 -0
  31. package/dist/architecture/context.js +116 -0
  32. package/dist/architecture/dc-generator.d.ts +9 -0
  33. package/dist/architecture/dc-generator.d.ts.map +1 -0
  34. package/dist/architecture/dc-generator.js +220 -0
  35. package/dist/architecture/definition-schema.d.ts +128 -0
  36. package/dist/architecture/definition-schema.d.ts.map +1 -0
  37. package/dist/architecture/definition-schema.js +94 -0
  38. package/dist/architecture/edge-detector-html.d.ts +6 -0
  39. package/dist/architecture/edge-detector-html.d.ts.map +1 -0
  40. package/dist/architecture/edge-detector-html.js +5 -0
  41. package/dist/architecture/edge-detector-web.d.ts +32 -0
  42. package/dist/architecture/edge-detector-web.d.ts.map +1 -0
  43. package/dist/architecture/edge-detector-web.js +133 -0
  44. package/dist/architecture/edge-detector.d.ts +116 -0
  45. package/dist/architecture/edge-detector.d.ts.map +1 -0
  46. package/dist/architecture/edge-detector.js +229 -0
  47. package/dist/architecture/entry-detector.d.ts +44 -0
  48. package/dist/architecture/entry-detector.d.ts.map +1 -0
  49. package/dist/architecture/entry-detector.js +263 -0
  50. package/dist/architecture/index.d.ts +21 -0
  51. package/dist/architecture/index.d.ts.map +1 -0
  52. package/dist/architecture/index.js +48 -0
  53. package/dist/architecture/layer-detector.d.ts +60 -0
  54. package/dist/architecture/layer-detector.d.ts.map +1 -0
  55. package/dist/architecture/layer-detector.js +331 -0
  56. package/dist/architecture/rego-generator.d.ts +25 -0
  57. package/dist/architecture/rego-generator.d.ts.map +1 -0
  58. package/dist/architecture/rego-generator.js +229 -0
  59. package/dist/architecture/templates/index.d.ts +39 -0
  60. package/dist/architecture/templates/index.d.ts.map +1 -0
  61. package/dist/architecture/templates/index.js +124 -0
  62. package/dist/architecture/types.d.ts +280 -0
  63. package/dist/architecture/types.d.ts.map +1 -0
  64. package/dist/architecture/types.js +269 -0
  65. package/dist/architecture/yaml-parser.d.ts +13 -0
  66. package/dist/architecture/yaml-parser.d.ts.map +1 -0
  67. package/dist/architecture/yaml-parser.js +234 -0
  68. package/dist/config/constants.d.ts +9 -0
  69. package/dist/config/constants.d.ts.map +1 -0
  70. package/dist/config/constants.js +20 -0
  71. package/dist/config/index.d.ts +9 -0
  72. package/dist/config/index.d.ts.map +1 -0
  73. package/dist/config/index.js +8 -0
  74. package/dist/config/loader.d.ts +41 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +76 -0
  77. package/dist/config/nudge-config.d.ts +35 -0
  78. package/dist/config/nudge-config.d.ts.map +1 -0
  79. package/dist/config/nudge-config.js +34 -0
  80. package/dist/config/types.d.ts +30 -0
  81. package/dist/config/types.d.ts.map +1 -0
  82. package/dist/config/types.js +4 -0
  83. package/dist/contracts/index.d.ts +14 -0
  84. package/dist/contracts/index.d.ts.map +1 -0
  85. package/dist/contracts/index.js +13 -0
  86. package/dist/contracts/schemas/aps.schema.d.ts +269 -0
  87. package/dist/contracts/schemas/aps.schema.d.ts.map +1 -0
  88. package/dist/contracts/schemas/aps.schema.js +183 -0
  89. package/dist/contracts/schemas/index.d.ts +12 -0
  90. package/dist/contracts/schemas/index.d.ts.map +1 -0
  91. package/dist/contracts/schemas/index.js +14 -0
  92. package/dist/contracts/schemas/json-schema.d.ts +14 -0
  93. package/dist/contracts/schemas/json-schema.d.ts.map +1 -0
  94. package/dist/contracts/schemas/json-schema.js +31 -0
  95. package/dist/contracts/schemas/warning.schema.d.ts +171 -0
  96. package/dist/contracts/schemas/warning.schema.d.ts.map +1 -0
  97. package/dist/contracts/schemas/warning.schema.js +123 -0
  98. package/dist/contracts/types/gate.types.d.ts +194 -0
  99. package/dist/contracts/types/gate.types.d.ts.map +1 -0
  100. package/dist/contracts/types/gate.types.js +19 -0
  101. package/dist/contracts/types/index.d.ts +9 -0
  102. package/dist/contracts/types/index.d.ts.map +1 -0
  103. package/dist/contracts/types/index.js +8 -0
  104. package/dist/crypto/hash.d.ts +47 -0
  105. package/dist/crypto/hash.d.ts.map +1 -0
  106. package/dist/crypto/hash.js +110 -0
  107. package/dist/crypto/index.d.ts +7 -0
  108. package/dist/crypto/index.d.ts.map +1 -0
  109. package/dist/crypto/index.js +6 -0
  110. package/dist/drift/index.d.ts +6 -0
  111. package/dist/drift/index.d.ts.map +1 -0
  112. package/dist/drift/index.js +5 -0
  113. package/dist/drift/report-generator.d.ts +21 -0
  114. package/dist/drift/report-generator.d.ts.map +1 -0
  115. package/dist/drift/report-generator.js +240 -0
  116. package/dist/drift/snapshot-capture.d.ts +26 -0
  117. package/dist/drift/snapshot-capture.d.ts.map +1 -0
  118. package/dist/drift/snapshot-capture.js +195 -0
  119. package/dist/drift/snapshot-compare.d.ts +50 -0
  120. package/dist/drift/snapshot-compare.d.ts.map +1 -0
  121. package/dist/drift/snapshot-compare.js +142 -0
  122. package/dist/drift/snapshot-schema.d.ts +197 -0
  123. package/dist/drift/snapshot-schema.d.ts.map +1 -0
  124. package/dist/drift/snapshot-schema.js +193 -0
  125. package/dist/drift/snapshot-storage.d.ts +25 -0
  126. package/dist/drift/snapshot-storage.d.ts.map +1 -0
  127. package/dist/drift/snapshot-storage.js +179 -0
  128. package/dist/explain/antipattern-explainer.d.ts +4 -0
  129. package/dist/explain/antipattern-explainer.d.ts.map +1 -0
  130. package/dist/explain/antipattern-explainer.js +196 -0
  131. package/dist/explain/boundary-explainer.d.ts +5 -0
  132. package/dist/explain/boundary-explainer.d.ts.map +1 -0
  133. package/dist/explain/boundary-explainer.js +261 -0
  134. package/dist/explain/explain-service.d.ts +19 -0
  135. package/dist/explain/explain-service.d.ts.map +1 -0
  136. package/dist/explain/explain-service.js +106 -0
  137. package/dist/explain/index.d.ts +7 -0
  138. package/dist/explain/index.d.ts.map +1 -0
  139. package/dist/explain/index.js +5 -0
  140. package/dist/explain/template-loader.d.ts +9 -0
  141. package/dist/explain/template-loader.d.ts.map +1 -0
  142. package/dist/explain/template-loader.js +51 -0
  143. package/dist/explain/types.d.ts +46 -0
  144. package/dist/explain/types.d.ts.map +1 -0
  145. package/dist/explain/types.js +31 -0
  146. package/dist/index.d.ts +26 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +37 -0
  149. package/dist/provenance/collector.d.ts +86 -0
  150. package/dist/provenance/collector.d.ts.map +1 -0
  151. package/dist/provenance/collector.js +425 -0
  152. package/dist/provenance/git-ai-standard/git-notes.d.ts +85 -0
  153. package/dist/provenance/git-ai-standard/git-notes.d.ts.map +1 -0
  154. package/dist/provenance/git-ai-standard/git-notes.js +292 -0
  155. package/dist/provenance/git-ai-standard/index.d.ts +44 -0
  156. package/dist/provenance/git-ai-standard/index.d.ts.map +1 -0
  157. package/dist/provenance/git-ai-standard/index.js +47 -0
  158. package/dist/provenance/git-ai-standard/serializer.d.ts +54 -0
  159. package/dist/provenance/git-ai-standard/serializer.d.ts.map +1 -0
  160. package/dist/provenance/git-ai-standard/serializer.js +224 -0
  161. package/dist/provenance/git-ai-standard/session.d.ts +51 -0
  162. package/dist/provenance/git-ai-standard/session.d.ts.map +1 -0
  163. package/dist/provenance/git-ai-standard/session.js +118 -0
  164. package/dist/provenance/git-ai-standard/types.d.ts +173 -0
  165. package/dist/provenance/git-ai-standard/types.d.ts.map +1 -0
  166. package/dist/provenance/git-ai-standard/types.js +109 -0
  167. package/dist/provenance/index.d.ts +5 -0
  168. package/dist/provenance/index.d.ts.map +1 -0
  169. package/dist/provenance/index.js +6 -0
  170. package/dist/provenance/store.d.ts +83 -0
  171. package/dist/provenance/store.d.ts.map +1 -0
  172. package/dist/provenance/store.js +248 -0
  173. package/dist/provenance/types.d.ts +160 -0
  174. package/dist/provenance/types.d.ts.map +1 -0
  175. package/dist/provenance/types.js +112 -0
  176. package/dist/suppression/index.d.ts +4 -0
  177. package/dist/suppression/index.d.ts.map +1 -0
  178. package/dist/suppression/index.js +3 -0
  179. package/dist/suppression/parser.d.ts +31 -0
  180. package/dist/suppression/parser.d.ts.map +1 -0
  181. package/dist/suppression/parser.js +219 -0
  182. package/dist/suppression/service.d.ts +29 -0
  183. package/dist/suppression/service.d.ts.map +1 -0
  184. package/dist/suppression/service.js +132 -0
  185. package/dist/suppression/store.d.ts +61 -0
  186. package/dist/suppression/store.d.ts.map +1 -0
  187. package/dist/suppression/store.js +169 -0
  188. package/dist/utils/debug.d.ts +48 -0
  189. package/dist/utils/debug.d.ts.map +1 -0
  190. package/dist/utils/debug.js +100 -0
  191. package/dist/utils/index.d.ts +4 -0
  192. package/dist/utils/index.d.ts.map +1 -0
  193. package/dist/utils/index.js +3 -0
  194. package/dist/utils/path-safety.d.ts +21 -0
  195. package/dist/utils/path-safety.d.ts.map +1 -0
  196. package/dist/utils/path-safety.js +49 -0
  197. package/dist/utils/severity.d.ts +37 -0
  198. package/dist/utils/severity.d.ts.map +1 -0
  199. package/dist/utils/severity.js +22 -0
  200. package/dist/validation/aps-validator.d.ts +66 -0
  201. package/dist/validation/aps-validator.d.ts.map +1 -0
  202. package/dist/validation/aps-validator.js +173 -0
  203. package/dist/validation/errors.d.ts +52 -0
  204. package/dist/validation/errors.d.ts.map +1 -0
  205. package/dist/validation/errors.js +115 -0
  206. package/dist/validation/index.d.ts +8 -0
  207. package/dist/validation/index.d.ts.map +1 -0
  208. package/dist/validation/index.js +13 -0
  209. package/dist/warnings/index.d.ts +2 -0
  210. package/dist/warnings/index.d.ts.map +1 -0
  211. package/dist/warnings/index.js +1 -0
  212. package/dist/warnings/warning-id.d.ts +180 -0
  213. package/dist/warnings/warning-id.d.ts.map +1 -0
  214. package/dist/warnings/warning-id.js +257 -0
  215. package/package.json +79 -0
@@ -0,0 +1,292 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { tmpdir } from 'node:os';
4
+ import { serializeAuthorshipLog, parseAuthorshipLog } from './serializer.js';
5
+ import { createDebugger } from '../../utils/debug.js';
6
+ const debug = createDebugger('git-ai-notes');
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * Validate a git commit SHA or ref to prevent shell injection
10
+ * Allows: hex characters (SHA), alphanumeric, dash, underscore, slash, tilde, caret, @, dot
11
+ */
12
+ function isValidGitRef(ref) {
13
+ return /^[a-zA-Z0-9_.~^@/-]+$/.test(ref) && ref.length > 0 && ref.length <= 256;
14
+ }
15
+ /**
16
+ * Validate a git remote name to prevent shell injection
17
+ * Allows: alphanumeric, dash, underscore, dot
18
+ */
19
+ function isValidRemoteName(remote) {
20
+ return /^[a-zA-Z0-9_.-]+$/.test(remote) && remote.length > 0 && remote.length <= 128;
21
+ }
22
+ /**
23
+ * Validate a git revision range to prevent shell injection
24
+ * Allows: hex characters, alphanumeric, dash, underscore, dot, tilde, caret, @, colon
25
+ */
26
+ function isValidRevisionRange(range) {
27
+ return /^[a-zA-Z0-9_.~^@:-]+$/.test(range) && range.length > 0 && range.length <= 512;
28
+ }
29
+ /**
30
+ * Git Notes namespace for AI authorship logs
31
+ * Per Git AI Standard v3.0.0
32
+ */
33
+ export const NOTES_REF = 'refs/notes/ai';
34
+ /**
35
+ * Write an authorship log to Git Notes for a commit
36
+ *
37
+ * @param commitSha - The commit SHA to attach the note to
38
+ * @param log - The authorship log to write
39
+ * @param workspaceRoot - The repository root directory
40
+ */
41
+ export async function writeAuthorshipNote(commitSha, log, workspaceRoot) {
42
+ if (!isValidGitRef(commitSha)) {
43
+ throw new Error(`Invalid commit SHA: ${commitSha}`);
44
+ }
45
+ const content = serializeAuthorshipLog(log);
46
+ // Write content to a temp file in OS temp directory to avoid issues with
47
+ // worktrees/submodules where .git may be a file, not a directory
48
+ const { writeFile, unlink } = await import('node:fs/promises');
49
+ const { join } = await import('node:path');
50
+ const { randomUUID } = await import('node:crypto');
51
+ const tempFile = join(tmpdir(), `anvil-note-${randomUUID()}.tmp`);
52
+ try {
53
+ await writeFile(tempFile, content, 'utf-8');
54
+ await execFileAsync('git', ['notes', `--ref=${NOTES_REF}`, 'add', '-f', '-F', tempFile, '--', commitSha], {
55
+ cwd: workspaceRoot,
56
+ });
57
+ debug(`Wrote authorship note for commit ${commitSha.slice(0, 8)}`);
58
+ }
59
+ catch (error) {
60
+ debug('Failed to write authorship note', error);
61
+ throw new Error(`Failed to write authorship note: ${error}`);
62
+ }
63
+ finally {
64
+ // Clean up temp file
65
+ try {
66
+ await unlink(tempFile);
67
+ }
68
+ catch (cleanupError) {
69
+ debug('Failed to clean up temp file', { tempFile, error: cleanupError });
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Read an authorship log from Git Notes for a commit
75
+ *
76
+ * @param commitSha - The commit SHA to read the note from (or 'HEAD')
77
+ * @param workspaceRoot - The repository root directory
78
+ * @returns The parsed AuthorshipLog, or null if no note exists
79
+ */
80
+ export async function readAuthorshipNote(commitSha, workspaceRoot) {
81
+ if (!isValidGitRef(commitSha)) {
82
+ throw new Error(`Invalid commit SHA: ${commitSha}`);
83
+ }
84
+ try {
85
+ const { stdout } = await execFileAsync('git', ['notes', `--ref=${NOTES_REF}`, 'show', '--', commitSha], {
86
+ cwd: workspaceRoot,
87
+ });
88
+ return parseAuthorshipLog(stdout);
89
+ }
90
+ catch (error) {
91
+ // Note doesn't exist or other error
92
+ debug(`No authorship note found for commit ${commitSha.slice(0, 8)}`, error);
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * List all commits with authorship notes
98
+ *
99
+ * @param workspaceRoot - The repository root directory
100
+ * @returns Array of commit SHAs that have authorship notes
101
+ */
102
+ export async function listAuthorshipNotes(workspaceRoot) {
103
+ try {
104
+ const { stdout } = await execFileAsync('git', ['notes', `--ref=${NOTES_REF}`, 'list'], {
105
+ cwd: workspaceRoot,
106
+ });
107
+ // Format: <note-sha> <commit-sha>
108
+ return stdout
109
+ .trim()
110
+ .split('\n')
111
+ .filter((line) => line)
112
+ .map((line) => line.split(' ')[1])
113
+ .filter((sha) => !!sha);
114
+ }
115
+ catch (error) {
116
+ debug('Failed to list authorship notes', error);
117
+ return [];
118
+ }
119
+ }
120
+ /**
121
+ * Remove an authorship note from a commit
122
+ *
123
+ * @param commitSha - The commit SHA to remove the note from
124
+ * @param workspaceRoot - The repository root directory
125
+ * @returns true if the note was removed, false if it didn't exist
126
+ */
127
+ export async function removeAuthorshipNote(commitSha, workspaceRoot) {
128
+ if (!isValidGitRef(commitSha)) {
129
+ throw new Error(`Invalid commit SHA: ${commitSha}`);
130
+ }
131
+ try {
132
+ await execFileAsync('git', ['notes', `--ref=${NOTES_REF}`, 'remove', '--', commitSha], {
133
+ cwd: workspaceRoot,
134
+ });
135
+ debug(`Removed authorship note for commit ${commitSha.slice(0, 8)}`);
136
+ return true;
137
+ }
138
+ catch (error) {
139
+ debug('Failed to remove authorship note (may not exist)', error);
140
+ return false;
141
+ }
142
+ }
143
+ /**
144
+ * Copy authorship note when rebasing (from old SHA to new SHA)
145
+ *
146
+ * Updates the base_commit_sha in metadata to point to the new commit.
147
+ *
148
+ * @param fromSha - The original commit SHA
149
+ * @param toSha - The new commit SHA after rebase
150
+ * @param workspaceRoot - The repository root directory
151
+ * @returns true if the note was copied, false if source note didn't exist
152
+ */
153
+ export async function copyAuthorshipNote(fromSha, toSha, workspaceRoot) {
154
+ if (!isValidGitRef(fromSha) || !isValidGitRef(toSha)) {
155
+ throw new Error(`Invalid commit SHA: fromSha=${fromSha}, toSha=${toSha}`);
156
+ }
157
+ try {
158
+ const existingLog = await readAuthorshipNote(fromSha, workspaceRoot);
159
+ if (!existingLog)
160
+ return false;
161
+ // Resolve toSha to full 40-character SHA
162
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--', toSha], {
163
+ cwd: workspaceRoot,
164
+ });
165
+ const resolvedSha = stdout.trim();
166
+ // Update base_commit_sha in metadata to point to new commit
167
+ const updatedLog = {
168
+ ...existingLog,
169
+ metadata: {
170
+ ...existingLog.metadata,
171
+ base_commit_sha: resolvedSha,
172
+ },
173
+ };
174
+ await writeAuthorshipNote(toSha, updatedLog, workspaceRoot);
175
+ debug(`Copied authorship note from ${fromSha.slice(0, 8)} to ${toSha.slice(0, 8)}`);
176
+ return true;
177
+ }
178
+ catch (error) {
179
+ debug(`Failed to copy authorship note from ${fromSha} to ${toSha}`, error);
180
+ return false;
181
+ }
182
+ }
183
+ /**
184
+ * Push authorship notes to remote
185
+ *
186
+ * @param remote - Remote name (e.g., 'origin')
187
+ * @param workspaceRoot - The repository root directory
188
+ */
189
+ export async function pushAuthorshipNotes(remote, workspaceRoot) {
190
+ if (!isValidRemoteName(remote)) {
191
+ throw new Error(`Invalid remote name: ${remote}`);
192
+ }
193
+ try {
194
+ await execFileAsync('git', ['push', remote, NOTES_REF], {
195
+ cwd: workspaceRoot,
196
+ });
197
+ debug(`Pushed authorship notes to ${remote}`);
198
+ }
199
+ catch (error) {
200
+ debug('Failed to push authorship notes', error);
201
+ throw new Error(`Failed to push authorship notes: ${error}`);
202
+ }
203
+ }
204
+ /**
205
+ * Fetch authorship notes from remote
206
+ *
207
+ * @param remote - Remote name (e.g., 'origin')
208
+ * @param workspaceRoot - The repository root directory
209
+ */
210
+ export async function fetchAuthorshipNotes(remote, workspaceRoot) {
211
+ if (!isValidRemoteName(remote)) {
212
+ throw new Error(`Invalid remote name: ${remote}`);
213
+ }
214
+ try {
215
+ await execFileAsync('git', ['fetch', remote, `${NOTES_REF}:${NOTES_REF}`], {
216
+ cwd: workspaceRoot,
217
+ });
218
+ debug(`Fetched authorship notes from ${remote}`);
219
+ }
220
+ catch (error) {
221
+ debug('Failed to fetch authorship notes', error);
222
+ throw new Error(`Failed to fetch authorship notes: ${error}`);
223
+ }
224
+ }
225
+ /**
226
+ * Check if a commit has an authorship note
227
+ *
228
+ * @param commitSha - The commit SHA to check
229
+ * @param workspaceRoot - The repository root directory
230
+ * @returns true if the commit has an authorship note
231
+ */
232
+ export async function hasAuthorshipNote(commitSha, workspaceRoot) {
233
+ if (!isValidGitRef(commitSha)) {
234
+ throw new Error(`Invalid commit SHA: ${commitSha}`);
235
+ }
236
+ try {
237
+ await execFileAsync('git', ['notes', `--ref=${NOTES_REF}`, 'show', '--', commitSha], {
238
+ cwd: workspaceRoot,
239
+ });
240
+ return true;
241
+ }
242
+ catch {
243
+ return false;
244
+ }
245
+ }
246
+ /**
247
+ * Get summary statistics of AI authorship in a range of commits
248
+ *
249
+ * @param range - Git revision range (e.g., 'main..HEAD', 'HEAD~10..HEAD')
250
+ * @param workspaceRoot - The repository root directory
251
+ * @returns Summary object with counts
252
+ */
253
+ export async function getAuthorshipStats(range, workspaceRoot) {
254
+ if (!isValidRevisionRange(range)) {
255
+ throw new Error(`Invalid revision range: ${range}`);
256
+ }
257
+ const stats = {
258
+ totalCommits: 0,
259
+ commitsWithAI: 0,
260
+ totalAdditions: 0,
261
+ totalDeletions: 0,
262
+ tools: {},
263
+ };
264
+ try {
265
+ // Get list of commits in range
266
+ const { stdout } = await execFileAsync('git', ['rev-list', '--', range], {
267
+ cwd: workspaceRoot,
268
+ });
269
+ const commits = stdout.trim().split('\n').filter(Boolean);
270
+ stats.totalCommits = commits.length;
271
+ // Batch: get all commits that have authorship notes in a single git command
272
+ const notedCommits = new Set(await listAuthorshipNotes(workspaceRoot));
273
+ // Only read notes for commits that are both in the range AND have notes
274
+ const commitsWithNotes = commits.filter((commit) => notedCommits.has(commit));
275
+ for (const commit of commitsWithNotes) {
276
+ const log = await readAuthorshipNote(commit, workspaceRoot);
277
+ if (log) {
278
+ stats.commitsWithAI++;
279
+ for (const prompt of Object.values(log.metadata.prompts)) {
280
+ stats.totalAdditions += prompt.total_additions;
281
+ stats.totalDeletions += prompt.total_deletions;
282
+ const tool = prompt.agent_id.tool;
283
+ stats.tools[tool] = (stats.tools[tool] || 0) + 1;
284
+ }
285
+ }
286
+ }
287
+ }
288
+ catch (error) {
289
+ debug('Failed to get authorship stats', error);
290
+ }
291
+ return stats;
292
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Git AI Standard v3.0.0 Implementation
3
+ *
4
+ * This module provides support for tracking AI-generated code contributions
5
+ * using Git Notes under the refs/notes/ai namespace.
6
+ *
7
+ * @see https://github.com/git-ai-project/git-ai/blob/main/specs/git_ai_standard_v3.0.0.md
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import {
12
+ * writeAuthorshipNote,
13
+ * readAuthorshipNote,
14
+ * generateSessionHash,
15
+ * SCHEMA_VERSION,
16
+ * type AuthorshipLog,
17
+ * } from '@eddacraft/anvil-core';
18
+ *
19
+ * // Create an authorship log
20
+ * const log: AuthorshipLog = {
21
+ * attestations: {
22
+ * 'src/feature.ts': [
23
+ * { sessionHash: generateSessionHash('claude-code', 'session-123'), lineRanges: '1-50' }
24
+ * ]
25
+ * },
26
+ * metadata: {
27
+ * schema_version: SCHEMA_VERSION,
28
+ * base_commit_sha: 'abc123...',
29
+ * prompts: { ... }
30
+ * }
31
+ * };
32
+ *
33
+ * // Attach to commit
34
+ * await writeAuthorshipNote(commitSha, log, workspaceRoot);
35
+ *
36
+ * // Read back
37
+ * const retrieved = await readAuthorshipNote(commitSha, workspaceRoot);
38
+ * ```
39
+ */
40
+ export { SCHEMA_VERSION, SessionHashSchema, LineRangeSchema, FileAttestationSchema, AgentIdSchema, MessageTypeSchema, MessageSchema, PromptRecordSchema, AuthorshipMetadataSchema, AuthorshipLogSchema, type SessionHash, type LineRange, type FileAttestation, type AgentId, type MessageType, type Message, type PromptRecord, type AuthorshipMetadata, type AuthorshipLog, } from './types.js';
41
+ export { serializeAuthorshipLog, parseAuthorshipLog, isAuthorshipLog, expandLineRanges, compactLineRanges, } from './serializer.js';
42
+ export { generateSessionHash, sessionHashFromAgentId, createAgentId, detectCurrentAgent, createExplicitAgent, formatAgentId, } from './session.js';
43
+ export { NOTES_REF, writeAuthorshipNote, readAuthorshipNote, listAuthorshipNotes, removeAuthorshipNote, copyAuthorshipNote, pushAuthorshipNotes, fetchAuthorshipNotes, hasAuthorshipNote, getAuthorshipStats, } from './git-notes.js';
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/provenance/git-ai-standard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,GACd,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Git AI Standard v3.0.0 Implementation
3
+ *
4
+ * This module provides support for tracking AI-generated code contributions
5
+ * using Git Notes under the refs/notes/ai namespace.
6
+ *
7
+ * @see https://github.com/git-ai-project/git-ai/blob/main/specs/git_ai_standard_v3.0.0.md
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import {
12
+ * writeAuthorshipNote,
13
+ * readAuthorshipNote,
14
+ * generateSessionHash,
15
+ * SCHEMA_VERSION,
16
+ * type AuthorshipLog,
17
+ * } from '@eddacraft/anvil-core';
18
+ *
19
+ * // Create an authorship log
20
+ * const log: AuthorshipLog = {
21
+ * attestations: {
22
+ * 'src/feature.ts': [
23
+ * { sessionHash: generateSessionHash('claude-code', 'session-123'), lineRanges: '1-50' }
24
+ * ]
25
+ * },
26
+ * metadata: {
27
+ * schema_version: SCHEMA_VERSION,
28
+ * base_commit_sha: 'abc123...',
29
+ * prompts: { ... }
30
+ * }
31
+ * };
32
+ *
33
+ * // Attach to commit
34
+ * await writeAuthorshipNote(commitSha, log, workspaceRoot);
35
+ *
36
+ * // Read back
37
+ * const retrieved = await readAuthorshipNote(commitSha, workspaceRoot);
38
+ * ```
39
+ */
40
+ // Types
41
+ export { SCHEMA_VERSION, SessionHashSchema, LineRangeSchema, FileAttestationSchema, AgentIdSchema, MessageTypeSchema, MessageSchema, PromptRecordSchema, AuthorshipMetadataSchema, AuthorshipLogSchema, } from './types.js';
42
+ // Serialization
43
+ export { serializeAuthorshipLog, parseAuthorshipLog, isAuthorshipLog, expandLineRanges, compactLineRanges, } from './serializer.js';
44
+ // Session management
45
+ export { generateSessionHash, sessionHashFromAgentId, createAgentId, detectCurrentAgent, createExplicitAgent, formatAgentId, } from './session.js';
46
+ // Git Notes operations
47
+ export { NOTES_REF, writeAuthorshipNote, readAuthorshipNote, listAuthorshipNotes, removeAuthorshipNote, copyAuthorshipNote, pushAuthorshipNotes, fetchAuthorshipNotes, hasAuthorshipNote, getAuthorshipStats, } from './git-notes.js';
@@ -0,0 +1,54 @@
1
+ import type { AuthorshipLog } from './types.js';
2
+ /**
3
+ * Serialize an AuthorshipLog to Git AI Standard format
4
+ *
5
+ * The format consists of two sections separated by "---":
6
+ *
7
+ * ```
8
+ * file/path.ts
9
+ * a1b2c3d4e5f67890 1-50,55-60
10
+ * another/file.ts
11
+ * b2c3d4e5f67890a1 1-100
12
+ * ---
13
+ * {"schema_version":"authorship/3.0.0",...}
14
+ * ```
15
+ *
16
+ * File paths with spaces or special characters are quoted.
17
+ *
18
+ * @param log - The authorship log to serialize
19
+ * @returns The serialized log string
20
+ */
21
+ export declare function serializeAuthorshipLog(log: AuthorshipLog): string;
22
+ /**
23
+ * Parse a Git AI Standard authorship log
24
+ *
25
+ * @param content - The raw log content from Git Notes
26
+ * @returns The parsed AuthorshipLog
27
+ * @throws Error if the content is malformed
28
+ */
29
+ export declare function parseAuthorshipLog(content: string): AuthorshipLog;
30
+ /**
31
+ * Check if content looks like an authorship log
32
+ *
33
+ * @param content - Content to check
34
+ * @returns true if the content appears to be a valid authorship log
35
+ */
36
+ export declare function isAuthorshipLog(content: string): boolean;
37
+ /**
38
+ * Parse line ranges into an array of line numbers
39
+ *
40
+ * Validates each part and skips invalid entries with a warning rather than
41
+ * throwing. Empty parts, NaN values, and ranges where start > end are skipped.
42
+ *
43
+ * @param ranges - Line range string (e.g., "1-10,15,20-25")
44
+ * @returns Array of individual line numbers
45
+ */
46
+ export declare function expandLineRanges(ranges: string): number[];
47
+ /**
48
+ * Compact an array of line numbers into ranges
49
+ *
50
+ * @param lines - Array of line numbers
51
+ * @returns Compact range string (e.g., "1-10,15,20-25")
52
+ */
53
+ export declare function compactLineRanges(lines: number[]): string;
54
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../src/provenance/git-ai-standard/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AAMjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAyBjE;AASD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAuCjE;AAwDD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAMxD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA+CzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAsBzD"}
@@ -0,0 +1,224 @@
1
+ import { AuthorshipLogSchema, SCHEMA_VERSION } from './types.js';
2
+ import { createDebugger } from '../../utils/debug.js';
3
+ const debug = createDebugger('provenance');
4
+ /**
5
+ * Serialize an AuthorshipLog to Git AI Standard format
6
+ *
7
+ * The format consists of two sections separated by "---":
8
+ *
9
+ * ```
10
+ * file/path.ts
11
+ * a1b2c3d4e5f67890 1-50,55-60
12
+ * another/file.ts
13
+ * b2c3d4e5f67890a1 1-100
14
+ * ---
15
+ * {"schema_version":"authorship/3.0.0",...}
16
+ * ```
17
+ *
18
+ * File paths with spaces or special characters are quoted.
19
+ *
20
+ * @param log - The authorship log to serialize
21
+ * @returns The serialized log string
22
+ */
23
+ export function serializeAuthorshipLog(log) {
24
+ const lines = [];
25
+ // Attestation section
26
+ const sortedPaths = Object.keys(log.attestations).sort();
27
+ for (const filePath of sortedPaths) {
28
+ const attestations = log.attestations[filePath];
29
+ if (!attestations || attestations.length === 0)
30
+ continue;
31
+ // Quote paths with spaces or special characters
32
+ const quotedPath = needsQuoting(filePath) ? `"${filePath}"` : filePath;
33
+ lines.push(quotedPath);
34
+ for (const attestation of attestations) {
35
+ lines.push(` ${attestation.sessionHash} ${attestation.lineRanges}`);
36
+ }
37
+ }
38
+ // Separator
39
+ lines.push('---');
40
+ // Metadata section (JSON, pretty-printed for readability)
41
+ lines.push(JSON.stringify(log.metadata, null, 2));
42
+ return lines.join('\n');
43
+ }
44
+ /**
45
+ * Check if a file path needs to be quoted
46
+ */
47
+ function needsQuoting(path) {
48
+ return /[\s"'\\]/.test(path);
49
+ }
50
+ /**
51
+ * Parse a Git AI Standard authorship log
52
+ *
53
+ * @param content - The raw log content from Git Notes
54
+ * @returns The parsed AuthorshipLog
55
+ * @throws Error if the content is malformed
56
+ */
57
+ export function parseAuthorshipLog(content) {
58
+ const separatorIndex = content.indexOf('\n---\n');
59
+ if (separatorIndex === -1) {
60
+ // Try alternate separator (just --- at start of line)
61
+ const altIndex = content.indexOf('\n---');
62
+ if (altIndex === -1) {
63
+ throw new Error('Invalid authorship log: missing --- separator');
64
+ }
65
+ }
66
+ const actualSeparatorIndex = content.indexOf('\n---\n') !== -1 ? content.indexOf('\n---\n') : content.indexOf('\n---');
67
+ const attestationSection = content.slice(0, actualSeparatorIndex);
68
+ const metadataSection = content.slice(actualSeparatorIndex).replace(/^\n---\n?/, '');
69
+ // Parse attestations
70
+ const attestations = parseAttestationSection(attestationSection);
71
+ // Parse metadata JSON
72
+ const metadataJson = metadataSection.trim();
73
+ if (!metadataJson) {
74
+ throw new Error('Invalid authorship log: empty metadata section');
75
+ }
76
+ let metadata;
77
+ try {
78
+ metadata = JSON.parse(metadataJson);
79
+ }
80
+ catch (e) {
81
+ throw new Error(`Invalid authorship log: malformed JSON in metadata section - ${e}`);
82
+ }
83
+ // Validate with Zod schema
84
+ const result = AuthorshipLogSchema.safeParse({ attestations, metadata });
85
+ if (!result.success) {
86
+ throw new Error(`Invalid authorship log: ${result.error.message}`);
87
+ }
88
+ return result.data;
89
+ }
90
+ /**
91
+ * Parse the attestation section of an authorship log
92
+ */
93
+ function parseAttestationSection(section) {
94
+ const attestations = {};
95
+ let currentFile = null;
96
+ for (const line of section.split('\n')) {
97
+ if (!line.trim())
98
+ continue;
99
+ // Check if this is a file path line (not indented)
100
+ if (!line.startsWith(' ') && !line.startsWith('\t')) {
101
+ currentFile = parseFilePath(line);
102
+ attestations[currentFile] = [];
103
+ }
104
+ else if (currentFile) {
105
+ // This is an attestation entry line (indented)
106
+ const attestation = parseAttestationLine(line.trim());
107
+ if (attestation) {
108
+ attestations[currentFile].push(attestation);
109
+ }
110
+ }
111
+ }
112
+ return attestations;
113
+ }
114
+ /**
115
+ * Parse a file path, handling quoted paths
116
+ */
117
+ function parseFilePath(line) {
118
+ const trimmed = line.trim();
119
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
120
+ return trimmed.slice(1, -1);
121
+ }
122
+ return trimmed;
123
+ }
124
+ /**
125
+ * Parse an attestation entry line
126
+ *
127
+ * Format: <session-hash> <line-ranges>
128
+ * Example: a1b2c3d4e5f67890 1-50,55-60
129
+ */
130
+ function parseAttestationLine(line) {
131
+ // Match 7-16 hex characters followed by space and line ranges
132
+ const match = line.match(/^([a-f0-9]{7,16})\s+(.+)$/);
133
+ if (!match)
134
+ return null;
135
+ return {
136
+ sessionHash: match[1],
137
+ lineRanges: match[2],
138
+ };
139
+ }
140
+ /**
141
+ * Check if content looks like an authorship log
142
+ *
143
+ * @param content - Content to check
144
+ * @returns true if the content appears to be a valid authorship log
145
+ */
146
+ export function isAuthorshipLog(content) {
147
+ return (content.includes('\n---\n') &&
148
+ content.includes(`"schema_version"`) &&
149
+ content.includes(SCHEMA_VERSION));
150
+ }
151
+ /**
152
+ * Parse line ranges into an array of line numbers
153
+ *
154
+ * Validates each part and skips invalid entries with a warning rather than
155
+ * throwing. Empty parts, NaN values, and ranges where start > end are skipped.
156
+ *
157
+ * @param ranges - Line range string (e.g., "1-10,15,20-25")
158
+ * @returns Array of individual line numbers
159
+ */
160
+ export function expandLineRanges(ranges) {
161
+ const lines = [];
162
+ for (const part of ranges.split(',')) {
163
+ const trimmed = part.trim();
164
+ // Skip empty parts (e.g., trailing commas, double commas)
165
+ if (trimmed === '') {
166
+ continue;
167
+ }
168
+ if (trimmed.includes('-')) {
169
+ const segments = trimmed.split('-');
170
+ if (segments.length !== 2) {
171
+ debug(`[anvil] expandLineRanges: skipping invalid range part "${trimmed}"`);
172
+ continue;
173
+ }
174
+ const start = Number(segments[0]);
175
+ const end = Number(segments[1]);
176
+ if (!Number.isInteger(start) || !Number.isInteger(end)) {
177
+ debug(`[anvil] expandLineRanges: skipping non-integer range "${trimmed}"`);
178
+ continue;
179
+ }
180
+ if (start > end) {
181
+ debug(`[anvil] expandLineRanges: skipping invalid range "${trimmed}" (start > end)`);
182
+ continue;
183
+ }
184
+ for (let i = start; i <= end; i++) {
185
+ lines.push(i);
186
+ }
187
+ }
188
+ else {
189
+ const num = Number(trimmed);
190
+ if (!Number.isInteger(num)) {
191
+ debug(`[anvil] expandLineRanges: skipping non-integer value "${trimmed}"`);
192
+ continue;
193
+ }
194
+ lines.push(num);
195
+ }
196
+ }
197
+ return lines.sort((a, b) => a - b);
198
+ }
199
+ /**
200
+ * Compact an array of line numbers into ranges
201
+ *
202
+ * @param lines - Array of line numbers
203
+ * @returns Compact range string (e.g., "1-10,15,20-25")
204
+ */
205
+ export function compactLineRanges(lines) {
206
+ if (lines.length === 0)
207
+ return '';
208
+ const sorted = [...new Set(lines)].sort((a, b) => a - b);
209
+ const ranges = [];
210
+ let rangeStart = sorted[0];
211
+ let rangeEnd = sorted[0];
212
+ for (let i = 1; i < sorted.length; i++) {
213
+ if (sorted[i] === rangeEnd + 1) {
214
+ rangeEnd = sorted[i];
215
+ }
216
+ else {
217
+ ranges.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
218
+ rangeStart = sorted[i];
219
+ rangeEnd = sorted[i];
220
+ }
221
+ }
222
+ ranges.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
223
+ return ranges.join(',');
224
+ }