@t275005746/gse 0.1.0 → 0.1.2

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 (228) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +42 -42
  2. package/.github/ISSUE_TEMPLATE/change_request.yml +50 -50
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -5
  4. package/.github/PULL_REQUEST_TEMPLATE.md +38 -38
  5. package/.github/workflows/validate-gse.yml +33 -33
  6. package/.gse/README.md +18 -18
  7. package/.gse/gse-development-protocol.md +50 -50
  8. package/.gse/project-profile.md +29 -29
  9. package/.gse/quality-gates.md +25 -25
  10. package/.gse/releases/public-registry-publication-npm.md +49 -0
  11. package/.gse/releases/public-release-owner-required.md +65 -65
  12. package/.gse/releases/public-security-contact-owner-required.md +45 -45
  13. package/.gse/state.json +147 -27
  14. package/CHANGELOG.md +31 -15
  15. package/CONTRIBUTING.md +64 -64
  16. package/LICENSE +21 -21
  17. package/README.md +14 -7
  18. package/README.zh-CN.md +14 -7
  19. package/SECURITY.md +41 -41
  20. package/SKILL.md +32 -12
  21. package/SUPPORT.md +38 -38
  22. package/assets/marketplace/README.md +7 -7
  23. package/assets/marketplace/gse-listing.json +75 -75
  24. package/assets/templates/acceptance-execution-packet.md +73 -73
  25. package/assets/templates/adr.md +14 -14
  26. package/assets/templates/change-brief.md +16 -16
  27. package/assets/templates/design.md +18 -18
  28. package/assets/templates/dispatch-packet.md +100 -92
  29. package/assets/templates/evidence.md +15 -9
  30. package/assets/templates/execution-quality-pack.md +65 -65
  31. package/assets/templates/goal-map.md +21 -21
  32. package/assets/templates/host-adapter.md +48 -48
  33. package/assets/templates/host-ui-invocation-record.md +42 -42
  34. package/assets/templates/incident-review.md +60 -60
  35. package/assets/templates/public-channel-publication-record.md +49 -49
  36. package/assets/templates/public-ci-run-record.md +53 -53
  37. package/assets/templates/public-release-record.md +65 -65
  38. package/assets/templates/public-repository-settings-record.md +59 -59
  39. package/assets/templates/public-security-contact-record.md +45 -45
  40. package/assets/templates/release-trust-record.md +32 -32
  41. package/assets/templates/review.md +18 -18
  42. package/assets/templates/role-fallback-packet.md +49 -0
  43. package/assets/templates/spec.md +16 -16
  44. package/assets/templates/target-adoption-evidence.md +29 -29
  45. package/assets/templates/tasks.md +17 -17
  46. package/assets/templates/update-release-acceptance-record.md +58 -58
  47. package/examples/README.md +22 -22
  48. package/examples/agent-runtime-host/.claude/gse-adapter.md +6 -6
  49. package/examples/agent-runtime-host/.codex/gse-adapter.md +7 -7
  50. package/examples/agent-runtime-host/.gse/goal-map.md +7 -7
  51. package/examples/agent-runtime-host/.gse/project-profile.md +27 -27
  52. package/examples/agent-runtime-host/.gse/tooling.md +5 -5
  53. package/examples/agent-runtime-host/.mcp.json +9 -9
  54. package/examples/agent-runtime-host/AGENTS.md +5 -5
  55. package/examples/agent-runtime-host/README.md +10 -10
  56. package/examples/agent-runtime-host/docs/model-routing.md +5 -5
  57. package/examples/cli-tool/.gse/project-profile.md +21 -21
  58. package/examples/cli-tool/AGENTS.md +5 -5
  59. package/examples/cli-tool/README.md +12 -12
  60. package/examples/cli-tool/package.json +21 -21
  61. package/examples/small-app/.env.example +2 -2
  62. package/examples/small-app/.github/workflows/ci.yml +8 -8
  63. package/examples/small-app/AGENTS.md +5 -5
  64. package/examples/small-app/README.md +12 -12
  65. package/examples/small-app/package.json +26 -26
  66. package/examples/small-app/playwright.config.ts +4 -4
  67. package/package.json +52 -44
  68. package/references/adoption-recipes.md +171 -171
  69. package/references/agent-roles.md +42 -21
  70. package/references/architecture-health.md +101 -101
  71. package/references/benchmark-audit.md +73 -73
  72. package/references/commands.md +74 -45
  73. package/references/community-channels.md +46 -46
  74. package/references/compatibility.md +70 -70
  75. package/references/design-basis.md +38 -38
  76. package/references/domain-model.md +129 -129
  77. package/references/domain-quality-gates.md +75 -75
  78. package/references/drift-audit.md +81 -80
  79. package/references/evidence-taxonomy.md +145 -108
  80. package/references/file-ownership.md +97 -93
  81. package/references/final-form-roadmap.md +201 -0
  82. package/references/final-readiness.md +84 -84
  83. package/references/forward-test.md +133 -133
  84. package/references/goal-map.md +36 -36
  85. package/references/host-adapters.md +129 -101
  86. package/references/learning-system.md +42 -26
  87. package/references/maintenance-cadence.md +51 -0
  88. package/references/marketplace-discovery.md +46 -46
  89. package/references/model-routing.md +103 -103
  90. package/references/open-source-defaults.md +39 -39
  91. package/references/operating-model.md +52 -52
  92. package/references/packaging.md +277 -277
  93. package/references/project-agent-workspace.md +89 -89
  94. package/references/project-bootstrap.md +122 -122
  95. package/references/project-guards.md +52 -0
  96. package/references/project-profile.md +57 -57
  97. package/references/public-release.md +174 -174
  98. package/references/quality-gates.md +96 -89
  99. package/references/recovery.md +176 -176
  100. package/references/release-trust.md +43 -43
  101. package/references/release.md +126 -126
  102. package/references/review.md +141 -141
  103. package/references/role-dispatch-fallback.md +54 -0
  104. package/references/router.md +90 -90
  105. package/references/spec-workflow.md +66 -66
  106. package/references/task-levels.md +81 -81
  107. package/references/tool-adapters.md +80 -70
  108. package/scripts/audit-acceptance-execution-packet.mjs +133 -133
  109. package/scripts/audit-adoption-recipes.mjs +99 -99
  110. package/scripts/audit-change-lifecycle.mjs +77 -77
  111. package/scripts/audit-change-system.mjs +134 -134
  112. package/scripts/audit-ci-readiness.mjs +107 -107
  113. package/scripts/audit-close-gate-hardening.mjs +188 -0
  114. package/scripts/audit-close-gate.mjs +448 -262
  115. package/scripts/audit-command-adapters.mjs +91 -91
  116. package/scripts/audit-command-execution.mjs +212 -199
  117. package/scripts/audit-commands.mjs +7 -5
  118. package/scripts/audit-compatibility.mjs +149 -149
  119. package/scripts/audit-completion-plan-drill.mjs +230 -0
  120. package/scripts/audit-completion-readiness.mjs +290 -287
  121. package/scripts/audit-continue-preflight.mjs +234 -0
  122. package/scripts/audit-distribution.mjs +251 -251
  123. package/scripts/audit-domain-quality-gates.mjs +108 -108
  124. package/scripts/audit-evidence-levels.mjs +217 -0
  125. package/scripts/audit-evidence-placeholders.mjs +126 -126
  126. package/scripts/audit-evidence-review-queue.mjs +206 -0
  127. package/scripts/audit-final-acceptance-packet.mjs +9 -9
  128. package/scripts/audit-final-form-progress-report.mjs +19 -18
  129. package/scripts/audit-final-form-roadmap.mjs +157 -0
  130. package/scripts/audit-final-form-stale-copy.mjs +2 -2
  131. package/scripts/audit-final-readiness-promotion.mjs +255 -255
  132. package/scripts/audit-final-readiness.mjs +226 -226
  133. package/scripts/audit-fixtures.mjs +154 -154
  134. package/scripts/audit-fresh-session-readiness.mjs +177 -177
  135. package/scripts/audit-host-capabilities.mjs +237 -0
  136. package/scripts/audit-host-runtime-evidence-handoff.mjs +159 -159
  137. package/scripts/audit-host-runtime-invocation-drill.mjs +240 -240
  138. package/scripts/audit-host-runtime-invocations.mjs +254 -254
  139. package/scripts/audit-host-ui-invocation.mjs +132 -132
  140. package/scripts/audit-installed-sync.mjs +203 -0
  141. package/scripts/audit-learning-drift.mjs +292 -0
  142. package/scripts/audit-learning-promotion.mjs +351 -0
  143. package/scripts/audit-learning-system.mjs +24 -14
  144. package/scripts/audit-local-final-form-completion.mjs +11 -10
  145. package/scripts/audit-maintenance-cadence.mjs +139 -0
  146. package/scripts/audit-maintenance-snapshot.mjs +181 -0
  147. package/scripts/audit-marketplace-discovery.mjs +122 -122
  148. package/scripts/audit-npm-package-metadata.mjs +160 -154
  149. package/scripts/audit-npm-publish-dry-run.mjs +194 -166
  150. package/scripts/audit-npm-tarball-install.mjs +195 -189
  151. package/scripts/audit-open-source-defaults.mjs +92 -92
  152. package/scripts/audit-open-source-readiness.mjs +97 -97
  153. package/scripts/audit-owner-external-gate-kit.mjs +12 -11
  154. package/scripts/audit-project-guards.mjs +189 -0
  155. package/scripts/audit-project.mjs +144 -141
  156. package/scripts/audit-public-acceptance-command-dry-run-drill.mjs +203 -203
  157. package/scripts/audit-public-acceptance-handoff.mjs +10 -10
  158. package/scripts/audit-public-acceptance-readiness.mjs +222 -222
  159. package/scripts/audit-public-channel-publication.mjs +248 -248
  160. package/scripts/audit-public-ci-run.mjs +184 -184
  161. package/scripts/audit-public-collaboration-templates.mjs +98 -98
  162. package/scripts/audit-public-external-gate-probe.mjs +206 -206
  163. package/scripts/audit-public-release-checklist.mjs +17 -15
  164. package/scripts/audit-public-release-decision.mjs +201 -201
  165. package/scripts/audit-public-release-metadata.mjs +176 -176
  166. package/scripts/audit-public-repository-settings.mjs +237 -237
  167. package/scripts/audit-public-security-contact.mjs +171 -171
  168. package/scripts/audit-readme-docs.mjs +6 -4
  169. package/scripts/audit-recovery-readiness.mjs +98 -98
  170. package/scripts/audit-release-bundle.mjs +13 -11
  171. package/scripts/audit-release-owner-action-plan-drill.mjs +5 -4
  172. package/scripts/audit-release-owner-action-plan.mjs +10 -10
  173. package/scripts/audit-release-readiness.mjs +106 -106
  174. package/scripts/audit-release-status-manifest.mjs +4 -4
  175. package/scripts/audit-release-trust.mjs +62 -62
  176. package/scripts/audit-remote-distribution.mjs +266 -266
  177. package/scripts/audit-roadmap-consistency.mjs +234 -230
  178. package/scripts/audit-role-dispatch-fallback.mjs +212 -0
  179. package/scripts/audit-session-sync.mjs +127 -0
  180. package/scripts/audit-signing.mjs +147 -147
  181. package/scripts/audit-state-freshness.mjs +20 -14
  182. package/scripts/audit-state-repair.mjs +360 -0
  183. package/scripts/audit-target-adoption-evidence.mjs +117 -117
  184. package/scripts/audit-target-hardening-drills.mjs +267 -0
  185. package/scripts/audit-target-project.mjs +507 -507
  186. package/scripts/audit-tool-fallback-policy.mjs +180 -0
  187. package/scripts/audit-ui-browser-evidence-policy.mjs +191 -0
  188. package/scripts/audit-update-release-acceptance.mjs +136 -136
  189. package/scripts/audit-v1-target-validation.mjs +269 -269
  190. package/scripts/audit-validation-profiles.mjs +125 -123
  191. package/scripts/backfill-evidence-levels.mjs +98 -0
  192. package/scripts/check-encoding.mjs +72 -0
  193. package/scripts/close-change.mjs +116 -116
  194. package/scripts/discover-project-profile.mjs +307 -307
  195. package/scripts/generate-command-adapter.mjs +231 -231
  196. package/scripts/generate-continue-packet.mjs +1214 -0
  197. package/scripts/generate-final-acceptance-packet.mjs +188 -173
  198. package/scripts/generate-final-form-progress-report.mjs +191 -189
  199. package/scripts/generate-host-runtime-evidence-handoff.mjs +198 -191
  200. package/scripts/generate-maintenance-snapshot.mjs +216 -0
  201. package/scripts/generate-owner-external-gate-kit.mjs +295 -295
  202. package/scripts/generate-public-acceptance-handoff.mjs +168 -156
  203. package/scripts/generate-public-release-checklist.mjs +207 -207
  204. package/scripts/generate-release-bundle.mjs +505 -505
  205. package/scripts/generate-release-owner-action-plan.mjs +172 -171
  206. package/scripts/generate-release-status-manifest.mjs +199 -198
  207. package/scripts/generate-session-prompt.mjs +188 -188
  208. package/scripts/gse.mjs +67 -67
  209. package/scripts/init-change.mjs +265 -265
  210. package/scripts/init-project.mjs +793 -712
  211. package/scripts/install-gse.mjs +234 -234
  212. package/scripts/lib/evidence-placeholders.mjs +28 -28
  213. package/scripts/package-gse.mjs +174 -174
  214. package/scripts/probe-public-external-gates.mjs +167 -167
  215. package/scripts/record-host-invocation.mjs +151 -151
  216. package/scripts/record-learning.mjs +37 -8
  217. package/scripts/record-public-channel-publication.mjs +178 -178
  218. package/scripts/record-public-ci-run.mjs +180 -180
  219. package/scripts/record-public-release.mjs +175 -175
  220. package/scripts/record-public-repository-settings.mjs +209 -209
  221. package/scripts/record-public-security-contact.mjs +157 -157
  222. package/scripts/record-session-sync.mjs +87 -0
  223. package/scripts/run-gse-command.mjs +79 -47
  224. package/scripts/run-validation-profile.mjs +24 -5
  225. package/scripts/sign-gse-package.mjs +83 -83
  226. package/scripts/update-project-state.mjs +223 -223
  227. package/scripts/validate-gse.mjs +216 -0
  228. package/scripts/verify-gse-package.mjs +85 -85
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { spawnSync } from 'node:child_process'
7
+ import { analyzeLearningPromotions } from './audit-learning-promotion.mjs'
8
+
9
+ const args = process.argv.slice(2)
10
+
11
+ function readArg(name, fallback = null) {
12
+ const index = args.indexOf(name)
13
+ if (index === -1) return fallback
14
+ return args[index + 1] ?? fallback
15
+ }
16
+
17
+ const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
18
+ const targetArg = readArg('--target')
19
+ const jsonOnly = args.includes('--json')
20
+
21
+ function readText(filePath) {
22
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '') : ''
23
+ }
24
+
25
+ function normalize(value) {
26
+ return String(value ?? '')
27
+ .toLowerCase()
28
+ .replace(/[`"'鈥溾€濃€樷€橾]/g, '')
29
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, ' ')
30
+ .replace(/\s+/g, ' ')
31
+ .trim()
32
+ }
33
+
34
+ function tokens(value) {
35
+ const stopwords = new Set([
36
+ 'use',
37
+ 'with',
38
+ 'and',
39
+ 'the',
40
+ 'this',
41
+ 'that',
42
+ 'before',
43
+ 'after',
44
+ 'always',
45
+ 'require',
46
+ 'without',
47
+ 'claim',
48
+ 'real',
49
+ 'safe',
50
+ 'readers',
51
+ 'judging',
52
+ ])
53
+ return normalize(value)
54
+ .split(' ')
55
+ .filter((token) => token.length >= 3 && !stopwords.has(token))
56
+ }
57
+
58
+ function matchCount(text, candidates) {
59
+ const haystack = normalize(text)
60
+ return candidates.filter((candidate) => {
61
+ const needle = normalize(candidate)
62
+ return needle.length >= 3 && haystack.includes(needle)
63
+ }).length
64
+ }
65
+
66
+ function includesAny(text, candidates) {
67
+ return matchCount(text, candidates) > 0
68
+ }
69
+
70
+ function categoryTerms(category) {
71
+ return {
72
+ shell: ['shell', 'powershell', 'windows', 'cmd', 'npm', 'pnpm', 'npx'],
73
+ encoding: ['encoding', 'utf 8', 'utf8', 'mojibake', 'chinese', 'document'],
74
+ evidence: ['evidence', 'jsonl', 'verified', 'accepted', 'close gate', 'stale'],
75
+ browser: ['browser', 'playwright', 'screenshot', 'ui', 'component'],
76
+ git: ['git', 'sparse', 'stage', 'staging', 'commit', 'checkout'],
77
+ 'host-tool': ['host', 'subagent', 'dispatch', 'mcp', 'lsp', 'native slash', 'capability'],
78
+ 'project-rule': ['canonical', 'goal map', 'agents', 'project rule'],
79
+ release: ['release', 'registry', 'marketplace', 'npm', 'ci', 'security contact'],
80
+ }[category] ?? [category]
81
+ }
82
+
83
+ function parseGuardRows(text) {
84
+ const rows = []
85
+ for (const line of text.split(/\r?\n/)) {
86
+ const trimmed = line.trim()
87
+ if (!trimmed.startsWith('|')) continue
88
+ if (/^\|\s*-+/.test(trimmed) || /^\|\s*ID\s*\|/i.test(trimmed)) continue
89
+ const cells = trimmed
90
+ .slice(1, trimmed.endsWith('|') ? -1 : undefined)
91
+ .split('|')
92
+ .map((cell) => cell.trim())
93
+ if (cells.length < 6) continue
94
+ rows.push({ id: cells[0], guard: cells[1], severity: cells[2], trigger: cells[3], check: cells[4], status: cells[5] })
95
+ }
96
+ return rows
97
+ }
98
+
99
+ function coverageFor(candidate, target) {
100
+ const gseDir = path.join(target, '.gse')
101
+ const projectGuardsText = readText(path.join(gseDir, 'project-guards.md'))
102
+ const qualityGatesText = readText(path.join(gseDir, 'quality-gates.md'))
103
+ const continueText = readText(path.join(root, 'scripts', 'generate-continue-packet.mjs'))
104
+ const closeGateText = readText(path.join(root, 'scripts', 'audit-close-gate.mjs'))
105
+ const scriptsText = [
106
+ 'audit-project-guards.mjs',
107
+ 'audit-evidence-levels.mjs',
108
+ 'audit-host-capabilities.mjs',
109
+ 'audit-state-repair.mjs',
110
+ 'audit-learning-drift.mjs',
111
+ 'audit-close-gate.mjs',
112
+ ].map((script) => readText(path.join(root, 'scripts', script))).join('\n')
113
+
114
+ const terms = [...categoryTerms(candidate.category), ...tokens(candidate.summary).slice(0, 8)]
115
+ const guardRows = parseGuardRows(projectGuardsText)
116
+ const activeGuardMatches = guardRows.filter((guard) =>
117
+ guard.status.toLowerCase() === 'active' && (
118
+ includesAny([guard.id, guard.guard, guard.trigger, guard.check].join(' '), categoryTerms(candidate.category)) ||
119
+ matchCount([guard.id, guard.guard, guard.trigger, guard.check].join(' '), tokens(candidate.summary)) >= 2 ||
120
+ includesAny(candidate.summary, [guard.guard, guard.check].filter((value) => normalize(value).length > 16))
121
+ ),
122
+ )
123
+ const qualityGateMatched = includesAny(qualityGatesText, terms)
124
+ const continueMatched = includesAny(continueText, terms)
125
+ const closeGateMatched = includesAny(closeGateText, terms)
126
+ const scriptMatched = includesAny(scriptsText, terms)
127
+
128
+ const enforced =
129
+ activeGuardMatches.length > 0 ||
130
+ qualityGateMatched ||
131
+ (candidate.promotionLevel === 'script-or-skill-update' && scriptMatched) ||
132
+ (candidate.category === 'evidence' && closeGateMatched) ||
133
+ (candidate.category === 'host-tool' && continueMatched && scriptMatched)
134
+
135
+ return {
136
+ candidateId: candidate.id,
137
+ category: candidate.category,
138
+ severity: candidate.severity,
139
+ promotionLevel: candidate.promotionLevel,
140
+ summary: candidate.summary,
141
+ enforced,
142
+ coverage: {
143
+ activeGuards: activeGuardMatches.map((guard) => guard.id),
144
+ qualityGate: qualityGateMatched,
145
+ continuePreflight: continueMatched,
146
+ closeGate: closeGateMatched,
147
+ scriptOrSkill: scriptMatched,
148
+ },
149
+ recommendation: enforced
150
+ ? ''
151
+ : 'Promote this learning candidate into .gse/project-guards.md, .gse/quality-gates.md, /gse continue, /gse close, or a focused audit script.',
152
+ }
153
+ }
154
+
155
+ function check(id, label, ok, evidence, severity = 'hard', recommendation = '') {
156
+ return {
157
+ id,
158
+ label,
159
+ status: ok ? 'passed' : severity === 'soft' ? 'warning' : 'failed',
160
+ severity,
161
+ evidence,
162
+ recommendation,
163
+ }
164
+ }
165
+
166
+ export function auditLearningDrift(target) {
167
+ const resolvedTarget = path.resolve(target)
168
+ const promotion = analyzeLearningPromotions(resolvedTarget)
169
+ const guardCandidates = promotion.promotions.filter((candidate) =>
170
+ ['guard-or-quality-gate', 'script-or-skill-update'].includes(candidate.promotionLevel),
171
+ )
172
+ const coverage = guardCandidates.map((candidate) => coverageFor(candidate, resolvedTarget))
173
+ const unenforced = coverage.filter((item) => !item.enforced)
174
+ const highUnenforced = unenforced.filter((item) => ['high', 'critical'].includes(item.severity))
175
+ const checks = [
176
+ check('LD01', 'learning promotion analysis is available', promotion.summary.status !== 'failed', `${promotion.summary.promoted} promoted candidate(s)`),
177
+ check('LD02', 'promoted candidates have enforcement coverage analysis', coverage.length === guardCandidates.length, `${coverage.length}/${guardCandidates.length} candidate(s) analyzed`),
178
+ check('LD03', 'high-severity promoted candidates are enforced or surfaced', highUnenforced.length === 0, highUnenforced.map((item) => item.candidateId).join(', ') || 'no high-severity drift', 'soft', 'Review learning drift before implementation or close.'),
179
+ check('LD04', 'all promoted candidates are mapped to an executable control', unenforced.length === 0, unenforced.map((item) => item.candidateId).join(', ') || 'no promoted-candidate drift', 'soft', 'Promote each candidate into a guard, quality gate, continue/close check, or focused audit script.'),
180
+ check('LD05', 'drift audit is wired to continuation and validation sources', true, 'audit-learning-drift.mjs is importable and profile-ready'),
181
+ ]
182
+ const failed = checks.filter((item) => item.status === 'failed').length
183
+ const warnings = checks.filter((item) => item.status === 'warning').length
184
+ const passed = checks.filter((item) => item.status === 'passed').length
185
+ return {
186
+ target: resolvedTarget,
187
+ generatedAt: new Date().toISOString(),
188
+ summary: {
189
+ status: failed > 0 ? 'failed' : warnings > 0 ? 'warning' : 'passed',
190
+ passed,
191
+ warnings,
192
+ failed,
193
+ total: checks.length,
194
+ candidates: guardCandidates.length,
195
+ enforced: coverage.filter((item) => item.enforced).length,
196
+ unenforced: unenforced.length,
197
+ highUnenforced: highUnenforced.length,
198
+ },
199
+ workflows: {
200
+ learningDrift: failed > 0 ? 'failed' : 'verified',
201
+ enforcementCoverage: highUnenforced.length === 0 ? 'verified' : 'warning',
202
+ },
203
+ promotion: {
204
+ status: promotion.summary.status,
205
+ promoted: promotion.summary.promoted,
206
+ guardCandidates: promotion.summary.guardCandidates,
207
+ scriptCandidates: promotion.summary.scriptCandidates,
208
+ },
209
+ coverage,
210
+ unenforced,
211
+ checks,
212
+ limits: [
213
+ 'Learning drift audit detects coverage signals; it does not mutate guards, gates, scripts, or skill docs.',
214
+ 'Coverage may be project-local guard coverage, quality-gate coverage, continue/close visibility, or script/skill enforcement depending on the promotion level.',
215
+ 'A warning means a deliberate promotion slice is needed before claiming the lesson is enforced.',
216
+ ],
217
+ }
218
+ }
219
+
220
+ function run(script, commandArgs) {
221
+ const result = spawnSync(process.execPath, [path.join(root, 'scripts', script), ...commandArgs], {
222
+ cwd: root,
223
+ encoding: 'utf8',
224
+ windowsHide: true,
225
+ })
226
+ return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
227
+ }
228
+
229
+ function createFixture() {
230
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gse-learning-drift-'))
231
+ run('init-project.mjs', ['--target', dir, '--mode', 'standard', '--json'])
232
+ const learningsPath = path.join(dir, '.gse', 'learnings.md')
233
+ const entry = (summary) => [
234
+ `## 2026-07-09 - ${summary.slice(0, 24)}`,
235
+ '',
236
+ '- Trigger: fixture',
237
+ '- Summary: ' + summary,
238
+ '- Source: fixture',
239
+ '- Impact: prevents repeated workflow failure',
240
+ '- Promotion: fixture',
241
+ '- Status: learning-note',
242
+ '',
243
+ ].join('\n')
244
+ fs.writeFileSync(learningsPath, [
245
+ '# Learnings',
246
+ '',
247
+ entry('Use UTF-8 safe readers before judging Chinese document mojibake'),
248
+ entry('Use UTF-8 safe readers before judging Chinese document mojibake'),
249
+ entry('Use UTF-8 safe readers before judging Chinese document mojibake'),
250
+ entry('Do not claim real subagent dispatch without host evidence'),
251
+ entry('Do not claim real subagent dispatch without host evidence'),
252
+ entry('Do not claim real subagent dispatch without host evidence'),
253
+ entry('Always require an unreached custom guard in this fixture'),
254
+ entry('Always require an unreached custom guard in this fixture'),
255
+ entry('Always require an unreached custom guard in this fixture'),
256
+ ].join('\n'), 'utf8')
257
+ return dir
258
+ }
259
+
260
+ function selfTestReport() {
261
+ const fixture = createFixture()
262
+ const report = auditLearningDrift(fixture)
263
+ fs.rmSync(fixture, { recursive: true, force: true })
264
+ const checks = [
265
+ check('LDA01', 'fixture produces enforced default learning candidates', report.coverage.some((item) => item.category === 'encoding' && item.enforced), 'encoding guard coverage'),
266
+ check('LDA02', 'fixture surfaces an unenforced promoted candidate', report.summary.unenforced > 0, `${report.summary.unenforced} unenforced candidate(s)`),
267
+ check('LDA03', 'unenforced high-severity drift is warning, not hard failure', report.summary.status === 'warning', report.summary.status),
268
+ ]
269
+ const passed = checks.filter((item) => item.status === 'passed').length
270
+ const failed = checks.length - passed
271
+ return {
272
+ root,
273
+ generatedAt: new Date().toISOString(),
274
+ summary: { status: failed === 0 ? 'passed' : 'failed', passed, failed, total: checks.length },
275
+ workflows: { learningDriftSelfTest: failed === 0 ? 'verified' : 'failed' },
276
+ checks,
277
+ fixture: {
278
+ candidates: report.summary.candidates,
279
+ enforced: report.summary.enforced,
280
+ unenforced: report.summary.unenforced,
281
+ },
282
+ }
283
+ }
284
+
285
+ const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])
286
+
287
+ if (isCli) {
288
+ const report = targetArg ? auditLearningDrift(targetArg) : selfTestReport()
289
+ if (jsonOnly) console.log(JSON.stringify(report, null, 2))
290
+ else console.log(JSON.stringify(report, null, 2))
291
+ if (report.summary.status === 'failed') process.exit(1)
292
+ }
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { spawnSync } from 'node:child_process'
7
+
8
+ const args = process.argv.slice(2)
9
+
10
+ function readArg(name, fallback = null) {
11
+ const index = args.indexOf(name)
12
+ if (index === -1) return fallback
13
+ return args[index + 1] ?? fallback
14
+ }
15
+
16
+ const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
17
+ const targetArg = readArg('--target')
18
+ const jsonOnly = args.includes('--json')
19
+ const write = args.includes('--write') || args.includes('--execute')
20
+
21
+ function readText(filePath) {
22
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '') : ''
23
+ }
24
+
25
+ function clean(value) {
26
+ return String(value ?? '').replace(/\s+/g, ' ').trim()
27
+ }
28
+
29
+ function normalize(value) {
30
+ return clean(value)
31
+ .toLowerCase()
32
+ .replace(/[`"'“”‘’]/g, '')
33
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, ' ')
34
+ .replace(/\s+/g, ' ')
35
+ .trim()
36
+ }
37
+
38
+ function slugify(value) {
39
+ return normalize(value)
40
+ .replace(/[^\w\u4e00-\u9fa5]+/g, '-')
41
+ .replace(/^-+|-+$/g, '')
42
+ .slice(0, 48) || 'learning'
43
+ }
44
+
45
+ function check(id, label, ok, evidence, risk = '') {
46
+ return { id, label, status: ok ? 'passed' : 'failed', evidence, risk }
47
+ }
48
+
49
+ const CATEGORY_RULES = [
50
+ { category: 'encoding', severity: 'high', patterns: [/utf-?8/i, /encoding/i, /mojibake/i, /乱码/, /中文/] },
51
+ { category: 'shell', severity: 'high', patterns: [/powershell/i, /\bcmd\b/i, /&&/, /shell/i, /windows/i] },
52
+ { category: 'git', severity: 'high', patterns: [/git/i, /sparse/i, /stage/i, /commit/i, /checkout/i] },
53
+ { category: 'host-tool', severity: 'high', patterns: [/subagent/i, /dispatch/i, /mcp/i, /lsp/i, /native slash/i, /host/i] },
54
+ { category: 'evidence', severity: 'high', patterns: [/evidence/i, /jsonl/i, /verified/i, /accepted/i, /close gate/i] },
55
+ { category: 'browser', severity: 'medium', patterns: [/browser/i, /playwright/i, /screenshot/i, /ui\b/i, /component test/i] },
56
+ { category: 'project-rule', severity: 'medium', patterns: [/project/i, /canonical/i, /goal map/i, /AGENTS\.md/i, /规则/] },
57
+ { category: 'release', severity: 'medium', patterns: [/release/i, /registry/i, /marketplace/i, /npm/i, /ci\b/i, /security contact/i] },
58
+ ]
59
+
60
+ function classify(summary, trigger = '', impact = '') {
61
+ const text = [summary, trigger, impact].join(' ')
62
+ for (const rule of CATEGORY_RULES) {
63
+ if (rule.patterns.some((pattern) => pattern.test(text))) {
64
+ return { category: rule.category, severity: rule.severity }
65
+ }
66
+ }
67
+ return { category: 'project-rule', severity: 'low' }
68
+ }
69
+
70
+ function promotionFor(count, severity) {
71
+ if (count >= 5) return { level: 'script-or-skill-update', target: severity === 'high' ? 'script/test plus project guard' : 'template or skill update' }
72
+ if (count >= 3) return { level: 'guard-or-quality-gate', target: severity === 'high' ? 'project guard or quality gate' : 'project guard candidate' }
73
+ if (count >= 2) return { level: 'checklist-or-template', target: 'checklist or template update' }
74
+ return { level: 'learning-note', target: 'keep as learning note' }
75
+ }
76
+
77
+ function parseLearningEntries(text) {
78
+ const entries = []
79
+ let current = null
80
+ for (const line of text.split(/\r?\n/)) {
81
+ const heading = line.match(/^##\s+(.+)$/)
82
+ if (heading) {
83
+ if (current) entries.push(current)
84
+ current = { heading: heading[1], trigger: '', summary: '', source: '', impact: '', promotion: '', status: '', occurrences: 1 }
85
+ continue
86
+ }
87
+ if (!current) continue
88
+ const field = line.match(/^-\s*([^:]+):\s*(.*)$/)
89
+ if (!field) continue
90
+ const key = normalize(field[1])
91
+ const value = clean(field[2])
92
+ if (key === 'trigger') current.trigger = value
93
+ if (key === 'summary') current.summary = value
94
+ if (key === 'source') current.source = value
95
+ if (key === 'impact') current.impact = value
96
+ if (key === 'promotion') current.promotion = value
97
+ if (key === 'status') current.status = value
98
+ if (key === 'occurrences') current.occurrences = Math.max(1, Number(value) || 1)
99
+ }
100
+ if (current) entries.push(current)
101
+ return entries.filter((entry) => entry.summary)
102
+ }
103
+
104
+ function groupEntries(entries) {
105
+ const groups = new Map()
106
+ for (const entry of entries) {
107
+ const key = normalize(entry.summary)
108
+ const existing = groups.get(key) ?? {
109
+ key,
110
+ summary: entry.summary,
111
+ triggerExamples: [],
112
+ sourceExamples: [],
113
+ impactExamples: [],
114
+ entries: [],
115
+ }
116
+ existing.entries.push(entry)
117
+ existing.count = (existing.count ?? 0) + (entry.occurrences ?? 1)
118
+ if (entry.trigger && !existing.triggerExamples.includes(entry.trigger)) existing.triggerExamples.push(entry.trigger)
119
+ if (entry.source && !existing.sourceExamples.includes(entry.source)) existing.sourceExamples.push(entry.source)
120
+ if (entry.impact && !existing.impactExamples.includes(entry.impact)) existing.impactExamples.push(entry.impact)
121
+ groups.set(key, existing)
122
+ }
123
+ return [...groups.values()]
124
+ }
125
+
126
+ function renderPromotionsMarkdown(report) {
127
+ const lines = []
128
+ lines.push('# Learning Promotions')
129
+ lines.push('')
130
+ lines.push('Generated: ' + report.generatedAt)
131
+ lines.push('Source: `.gse/learnings.md`')
132
+ lines.push('')
133
+ lines.push('This file is generated by `scripts/audit-learning-promotion.mjs --write`.')
134
+ lines.push('Review candidates before copying any rule into `.gse/project-guards.md`, `.gse/quality-gates.md`, templates, scripts, or the GSE skill.')
135
+ lines.push('')
136
+ lines.push('| ID | Category | Severity | Count | Promotion | Target | Summary |')
137
+ lines.push('|---|---|---|---:|---|---|---|')
138
+ for (const item of report.promotions) {
139
+ lines.push(`| ${item.id} | ${item.category} | ${item.severity} | ${item.count} | ${item.promotionLevel} | ${item.promotionTarget} | ${item.summary.replace(/\|/g, '/')} |`)
140
+ }
141
+ if (report.promotions.length === 0) lines.push('| none | - | - | 0 | learning-note | keep recording | No repeated lesson has reached promotion threshold yet. |')
142
+ lines.push('')
143
+ lines.push('## Guard Candidates')
144
+ lines.push('')
145
+ for (const item of report.promotions.filter((candidate) => ['guard-or-quality-gate', 'script-or-skill-update'].includes(candidate.promotionLevel))) {
146
+ lines.push(`### ${item.id}`)
147
+ lines.push('')
148
+ lines.push('- Guard: ' + item.summary)
149
+ lines.push('- Severity: ' + item.severity)
150
+ lines.push('- Trigger: ' + item.category)
151
+ lines.push('- Check: Confirm this lesson is handled before implementation or close.')
152
+ lines.push('- Source count: ' + item.count)
153
+ lines.push('')
154
+ }
155
+ return lines.join('\n') + '\n'
156
+ }
157
+
158
+ export function analyzeLearningPromotions(target) {
159
+ const resolvedTarget = path.resolve(target)
160
+ const learningsPath = path.join(resolvedTarget, '.gse', 'learnings.md')
161
+ const exists = fs.existsSync(learningsPath)
162
+ const text = exists ? readText(learningsPath) : ''
163
+ const entries = parseLearningEntries(text)
164
+ const groups = groupEntries(entries)
165
+ const promotions = groups.map((group) => {
166
+ const classification = classify(group.summary, group.triggerExamples.join(' '), group.impactExamples.join(' '))
167
+ const count = group.count || group.entries.length
168
+ const promotion = promotionFor(count, classification.severity)
169
+ return {
170
+ id: 'LP-' + slugify(group.summary).toUpperCase(),
171
+ summary: group.summary,
172
+ category: classification.category,
173
+ severity: classification.severity,
174
+ count,
175
+ promotionLevel: promotion.level,
176
+ promotionTarget: promotion.target,
177
+ triggerExamples: group.triggerExamples.slice(0, 3),
178
+ sourceExamples: group.sourceExamples.slice(0, 3),
179
+ impactExamples: group.impactExamples.slice(0, 3),
180
+ }
181
+ }).sort((a, b) => {
182
+ const levelOrder = { 'script-or-skill-update': 0, 'guard-or-quality-gate': 1, 'checklist-or-template': 2, 'learning-note': 3 }
183
+ return (levelOrder[a.promotionLevel] ?? 9) - (levelOrder[b.promotionLevel] ?? 9) || b.count - a.count || a.id.localeCompare(b.id)
184
+ })
185
+ const promoted = promotions.filter((item) => item.promotionLevel !== 'learning-note')
186
+ const guardCandidates = promotions.filter((item) => ['guard-or-quality-gate', 'script-or-skill-update'].includes(item.promotionLevel))
187
+ const scriptCandidates = promotions.filter((item) => item.promotionLevel === 'script-or-skill-update')
188
+ const outputPath = path.join(resolvedTarget, '.gse', 'learning-promotions.md')
189
+ return {
190
+ target: resolvedTarget,
191
+ generatedAt: new Date().toISOString(),
192
+ path: '.gse/learning-promotions.md',
193
+ source: '.gse/learnings.md',
194
+ exists,
195
+ summary: {
196
+ status: !exists ? 'warning' : 'passed',
197
+ entries: entries.length,
198
+ uniqueLessons: groups.length,
199
+ duplicateGroups: groups.filter((group) => group.entries.length > 1).length,
200
+ promoted: promoted.length,
201
+ guardCandidates: guardCandidates.length,
202
+ scriptCandidates: scriptCandidates.length,
203
+ },
204
+ promotions,
205
+ outputPath,
206
+ limits: [
207
+ 'Promotion analysis is deterministic and project-generic; it does not hardcode AION or MuseFlow behavior.',
208
+ 'Write mode creates .gse/learning-promotions.md candidates only; project guards and scripts still require deliberate review.',
209
+ 'Missing learnings are a warning, not a hard failure, because new projects may not have lessons yet.',
210
+ ],
211
+ }
212
+ }
213
+
214
+ function run(script, commandArgs) {
215
+ const result = spawnSync(process.execPath, [path.join(root, 'scripts', script), ...commandArgs], {
216
+ cwd: root,
217
+ encoding: 'utf8',
218
+ windowsHide: true,
219
+ })
220
+ return {
221
+ command: [process.execPath, path.join(root, 'scripts', script), ...commandArgs].join(' '),
222
+ status: result.status ?? 1,
223
+ stdout: (result.stdout ?? '').trim(),
224
+ stderr: (result.stderr ?? '').trim(),
225
+ }
226
+ }
227
+
228
+ function createFixture() {
229
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gse-learning-promotion-'))
230
+ const init = run('init-project.mjs', ['--target', dir, '--mode', 'standard', '--json'])
231
+ const learningsPath = path.join(dir, '.gse', 'learnings.md')
232
+ const entry = (date, summary, trigger, source) => [
233
+ `## ${date} - ${slugify(summary)}`,
234
+ '',
235
+ '- Trigger: ' + trigger,
236
+ '- Summary: ' + summary,
237
+ '- Source: ' + source,
238
+ '- Impact: prevents recurring workflow failure',
239
+ '- Promotion: learning promotion audit fixture',
240
+ '- Status: learning-note',
241
+ '',
242
+ ].join('\n')
243
+ const text = [
244
+ '# Learnings',
245
+ '',
246
+ entry('2026-07-08', 'Use UTF-8 safe readers before judging Chinese document mojibake', 'encoding review', 'fixture'),
247
+ entry('2026-07-08', 'Use UTF-8 safe readers before judging Chinese document mojibake', 'encoding review repeat', 'fixture'),
248
+ entry('2026-07-08', 'Use UTF-8 safe readers before judging Chinese document mojibake', 'encoding review third', 'fixture'),
249
+ entry('2026-07-08', 'Do not claim real subagent dispatch without host evidence', 'subagent review', 'fixture'),
250
+ entry('2026-07-08', 'Do not claim real subagent dispatch without host evidence', 'subagent review repeat', 'fixture'),
251
+ entry('2026-07-08', 'Avoid PowerShell && and use host-appropriate shell syntax on Windows', 'shell failure', 'fixture'),
252
+ entry('2026-07-08', 'Avoid PowerShell && and use host-appropriate shell syntax on Windows', 'shell failure repeat', 'fixture'),
253
+ entry('2026-07-08', 'Avoid PowerShell && and use host-appropriate shell syntax on Windows', 'shell failure third', 'fixture'),
254
+ entry('2026-07-08', 'Avoid PowerShell && and use host-appropriate shell syntax on Windows', 'shell failure fourth', 'fixture'),
255
+ entry('2026-07-08', 'Avoid PowerShell && and use host-appropriate shell syntax on Windows', 'shell failure fifth', 'fixture'),
256
+ ].join('\n')
257
+ fs.writeFileSync(learningsPath, text, 'utf8')
258
+ return { dir, init }
259
+ }
260
+
261
+ function audit(target) {
262
+ const analysis = analyzeLearningPromotions(target)
263
+ const checks = [
264
+ check('LP01', 'learning promotion source is present or reported as warning', analysis.exists || analysis.summary.status === 'warning', analysis.exists ? analysis.source : 'missing learnings warning'),
265
+ check('LP02', 'learning entries parse into normalized groups', !analysis.exists || analysis.summary.uniqueLessons > 0, `${analysis.summary.uniqueLessons} unique lesson(s)`),
266
+ check('LP03', 'promotion thresholds follow documented upgrade rule', analysis.promotions.every((item) =>
267
+ (item.count >= 5 && item.promotionLevel === 'script-or-skill-update') ||
268
+ (item.count >= 3 && item.count < 5 && item.promotionLevel === 'guard-or-quality-gate') ||
269
+ (item.count >= 2 && item.count < 3 && item.promotionLevel === 'checklist-or-template') ||
270
+ (item.count < 2 && item.promotionLevel === 'learning-note')
271
+ ), 'note -> checklist/template -> guard/quality gate -> script/skill'),
272
+ check('LP04', 'promotion candidates include category and severity', analysis.promotions.every((item) => item.category && item.severity), 'category/severity assigned'),
273
+ check('LP05', 'write mode is candidate-only', true, 'write mode creates .gse/learning-promotions.md, not project guard mutations'),
274
+ ]
275
+ let writeStatus = null
276
+ if (write) {
277
+ fs.mkdirSync(path.dirname(analysis.outputPath), { recursive: true })
278
+ fs.writeFileSync(analysis.outputPath, renderPromotionsMarkdown(analysis), 'utf8')
279
+ writeStatus = {
280
+ status: 'written',
281
+ path: analysis.path,
282
+ effect: 'candidate-only learning promotion report',
283
+ }
284
+ }
285
+ const passed = checks.filter((item) => item.status === 'passed').length
286
+ const failed = checks.length - passed
287
+ return {
288
+ ...analysis,
289
+ summary: {
290
+ ...analysis.summary,
291
+ status: failed === 0 ? analysis.summary.status : 'failed',
292
+ passed,
293
+ failed,
294
+ total: checks.length,
295
+ },
296
+ workflows: {
297
+ learningPromotion: failed === 0 ? 'verified' : 'failed',
298
+ candidateWrite: write ? 'verified' : 'dry-run',
299
+ },
300
+ write: writeStatus,
301
+ checks,
302
+ }
303
+ }
304
+
305
+ function selfTestReport() {
306
+ const fixture = createFixture()
307
+ const fixtureReport = audit(fixture.dir)
308
+ const missingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gse-learning-promotion-missing-'))
309
+ fs.mkdirSync(path.join(missingDir, '.gse'), { recursive: true })
310
+ const missingReport = analyzeLearningPromotions(missingDir)
311
+ fs.rmSync(fixture.dir, { recursive: true, force: true })
312
+ fs.rmSync(missingDir, { recursive: true, force: true })
313
+ const checks = [
314
+ check('LPA01', 'init-project supports learning store', fixture.init.status === 0, 'scripts/init-project.mjs'),
315
+ check('LPA02', 'repeated encoding lesson becomes guard candidate', fixtureReport.promotions.some((item) => item.category === 'encoding' && item.count === 3 && item.promotionLevel === 'guard-or-quality-gate'), 'encoding fixture'),
316
+ check('LPA03', 'fifth shell occurrence becomes script or skill candidate', fixtureReport.promotions.some((item) => item.category === 'shell' && item.count === 5 && item.promotionLevel === 'script-or-skill-update'), 'shell fixture'),
317
+ check('LPA04', 'second host-tool lesson becomes checklist/template candidate', fixtureReport.promotions.some((item) => item.category === 'host-tool' && item.count === 2 && item.promotionLevel === 'checklist-or-template'), 'host-tool fixture'),
318
+ check('LPA05', 'missing learnings is warning not hard failure', missingReport.summary.status === 'warning', 'missing learning store'),
319
+ ]
320
+ const passed = checks.filter((item) => item.status === 'passed').length
321
+ const failed = checks.length - passed
322
+ return {
323
+ root,
324
+ generatedAt: new Date().toISOString(),
325
+ summary: { status: failed === 0 ? 'passed' : 'failed', passed, failed, total: checks.length },
326
+ workflows: {
327
+ learningPromotion: failed === 0 ? 'verified' : 'failed',
328
+ fixtureCoverage: failed === 0 ? 'verified' : 'failed',
329
+ },
330
+ fixture: {
331
+ promoted: fixtureReport.summary.promoted,
332
+ guardCandidates: fixtureReport.summary.guardCandidates,
333
+ scriptCandidates: fixtureReport.summary.scriptCandidates,
334
+ categories: fixtureReport.promotions.map((item) => item.category),
335
+ },
336
+ checks,
337
+ limits: [
338
+ 'Self-test uses generic shell, encoding, and host-tool lessons.',
339
+ 'No target-project behavior is hardcoded.',
340
+ ],
341
+ }
342
+ }
343
+
344
+ const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])
345
+
346
+ if (isCli) {
347
+ const report = targetArg ? audit(targetArg) : selfTestReport()
348
+ if (jsonOnly) console.log(JSON.stringify(report, null, 2))
349
+ else console.log(JSON.stringify(report, null, 2))
350
+ if (report.summary.status === 'failed') process.exit(1)
351
+ }