@t275005746/gse 0.1.1 → 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 (84) hide show
  1. package/.gse/state.json +147 -26
  2. package/CHANGELOG.md +19 -3
  3. package/README.md +13 -6
  4. package/README.zh-CN.md +13 -6
  5. package/SKILL.md +32 -12
  6. package/assets/templates/dispatch-packet.md +22 -14
  7. package/assets/templates/evidence.md +11 -5
  8. package/assets/templates/role-fallback-packet.md +49 -0
  9. package/package.json +18 -10
  10. package/references/agent-roles.md +41 -20
  11. package/references/commands.md +74 -45
  12. package/references/drift-audit.md +2 -1
  13. package/references/evidence-taxonomy.md +46 -9
  14. package/references/file-ownership.md +17 -13
  15. package/references/final-form-roadmap.md +201 -0
  16. package/references/final-readiness.md +3 -3
  17. package/references/host-adapters.md +38 -10
  18. package/references/learning-system.md +24 -8
  19. package/references/maintenance-cadence.md +51 -0
  20. package/references/project-guards.md +52 -0
  21. package/references/quality-gates.md +18 -11
  22. package/references/role-dispatch-fallback.md +54 -0
  23. package/references/tool-adapters.md +13 -3
  24. package/scripts/audit-close-gate-hardening.mjs +188 -0
  25. package/scripts/audit-close-gate.mjs +219 -33
  26. package/scripts/audit-command-execution.mjs +30 -17
  27. package/scripts/audit-commands.mjs +7 -5
  28. package/scripts/audit-completion-plan-drill.mjs +230 -0
  29. package/scripts/audit-completion-readiness.mjs +9 -6
  30. package/scripts/audit-continue-preflight.mjs +234 -0
  31. package/scripts/audit-evidence-levels.mjs +217 -0
  32. package/scripts/audit-evidence-review-queue.mjs +206 -0
  33. package/scripts/audit-final-acceptance-packet.mjs +9 -9
  34. package/scripts/audit-final-form-progress-report.mjs +19 -18
  35. package/scripts/audit-final-form-roadmap.mjs +157 -0
  36. package/scripts/audit-final-form-stale-copy.mjs +2 -2
  37. package/scripts/audit-final-readiness.mjs +3 -3
  38. package/scripts/audit-fresh-session-readiness.mjs +1 -1
  39. package/scripts/audit-host-capabilities.mjs +237 -0
  40. package/scripts/audit-installed-sync.mjs +203 -0
  41. package/scripts/audit-learning-drift.mjs +292 -0
  42. package/scripts/audit-learning-promotion.mjs +351 -0
  43. package/scripts/audit-learning-system.mjs +24 -14
  44. package/scripts/audit-local-final-form-completion.mjs +11 -10
  45. package/scripts/audit-maintenance-cadence.mjs +139 -0
  46. package/scripts/audit-maintenance-snapshot.mjs +181 -0
  47. package/scripts/audit-npm-package-metadata.mjs +8 -2
  48. package/scripts/audit-npm-publish-dry-run.mjs +10 -4
  49. package/scripts/audit-npm-tarball-install.mjs +9 -3
  50. package/scripts/audit-owner-external-gate-kit.mjs +12 -11
  51. package/scripts/audit-project-guards.mjs +189 -0
  52. package/scripts/audit-project.mjs +14 -11
  53. package/scripts/audit-public-acceptance-handoff.mjs +10 -10
  54. package/scripts/audit-public-acceptance-readiness.mjs +6 -6
  55. package/scripts/audit-public-release-checklist.mjs +17 -15
  56. package/scripts/audit-release-bundle.mjs +13 -11
  57. package/scripts/audit-release-owner-action-plan-drill.mjs +5 -4
  58. package/scripts/audit-release-owner-action-plan.mjs +10 -10
  59. package/scripts/audit-release-status-manifest.mjs +4 -4
  60. package/scripts/audit-roadmap-consistency.mjs +11 -7
  61. package/scripts/audit-role-dispatch-fallback.mjs +212 -0
  62. package/scripts/audit-session-sync.mjs +127 -0
  63. package/scripts/audit-state-freshness.mjs +6 -5
  64. package/scripts/audit-state-repair.mjs +360 -0
  65. package/scripts/audit-target-hardening-drills.mjs +267 -0
  66. package/scripts/audit-tool-fallback-policy.mjs +180 -0
  67. package/scripts/audit-ui-browser-evidence-policy.mjs +191 -0
  68. package/scripts/audit-validation-profiles.mjs +6 -4
  69. package/scripts/backfill-evidence-levels.mjs +98 -0
  70. package/scripts/check-encoding.mjs +72 -0
  71. package/scripts/generate-continue-packet.mjs +1214 -0
  72. package/scripts/generate-final-acceptance-packet.mjs +23 -8
  73. package/scripts/generate-final-form-progress-report.mjs +25 -23
  74. package/scripts/generate-host-runtime-evidence-handoff.mjs +23 -16
  75. package/scripts/generate-maintenance-snapshot.mjs +216 -0
  76. package/scripts/generate-public-acceptance-handoff.mjs +26 -14
  77. package/scripts/generate-release-owner-action-plan.mjs +4 -3
  78. package/scripts/generate-release-status-manifest.mjs +4 -3
  79. package/scripts/init-project.mjs +144 -63
  80. package/scripts/record-learning.mjs +37 -8
  81. package/scripts/record-session-sync.mjs +87 -0
  82. package/scripts/run-gse-command.mjs +79 -47
  83. package/scripts/run-validation-profile.mjs +24 -5
  84. package/scripts/validate-gse.mjs +216 -0
@@ -0,0 +1,203 @@
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 crypto from 'node:crypto'
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 installedRootArg = readArg('--installed-root', process.env.GSE_INSTALLED_SKILL_ROOT || null)
18
+ const installedRoot = installedRootArg ? path.resolve(installedRootArg) : null
19
+ const jsonOnly = args.includes('--json')
20
+
21
+ function run(command, commandArgs, cwd = root) {
22
+ const result = spawnSync(command, commandArgs, {
23
+ cwd,
24
+ encoding: 'utf8',
25
+ windowsHide: true,
26
+ })
27
+ return {
28
+ command: [command, ...commandArgs].join(' '),
29
+ status: result.status ?? 1,
30
+ stdout: (result.stdout ?? '').trim(),
31
+ stderr: (result.stderr ?? '').trim(),
32
+ }
33
+ }
34
+
35
+ function parseJson(text) {
36
+ try {
37
+ return JSON.parse(text)
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ function exists(relativePath, base = root) {
44
+ return fs.existsSync(path.join(base, relativePath))
45
+ }
46
+
47
+ function sha256(filePath) {
48
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
49
+ }
50
+
51
+ function check(id, label, ok, evidence, risk = '') {
52
+ return { id, label, status: ok ? 'passed' : 'failed', evidence, risk }
53
+ }
54
+
55
+ function packageToTemp() {
56
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gse-installed-sync-'))
57
+ const packageOut = path.join(tempRoot, 'package')
58
+ const packageRun = run(process.execPath, [
59
+ path.join(root, 'scripts', 'package-gse.mjs'),
60
+ '--root',
61
+ root,
62
+ '--out',
63
+ packageOut,
64
+ '--label',
65
+ 'installed-sync',
66
+ '--force',
67
+ '--json',
68
+ ])
69
+ const manifestPath = path.join(packageOut, 'gse-package-manifest.json')
70
+ const manifest = fs.existsSync(manifestPath) ? parseJson(fs.readFileSync(manifestPath, 'utf8')) : null
71
+ return { tempRoot, packageOut, packageRun, manifest }
72
+ }
73
+
74
+ function compareInstalled(manifest, targetRoot) {
75
+ if (!targetRoot) return { compared: 0, missing: [], mismatched: [] }
76
+ const missing = []
77
+ const mismatched = []
78
+ const fileHashes = manifest?.fileHashes ?? {}
79
+ for (const [relativePath, expectedHash] of Object.entries(fileHashes)) {
80
+ const targetPath = path.join(targetRoot, relativePath)
81
+ if (!fs.existsSync(targetPath)) {
82
+ missing.push(relativePath)
83
+ continue
84
+ }
85
+ const actualHash = sha256(targetPath)
86
+ if (actualHash !== expectedHash) mismatched.push(relativePath)
87
+ }
88
+ return {
89
+ compared: Object.keys(fileHashes).length,
90
+ missing,
91
+ mismatched,
92
+ }
93
+ }
94
+
95
+ const { tempRoot, packageOut, packageRun, manifest } = packageToTemp()
96
+ const packageJson = exists('package.json') ? parseJson(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) : null
97
+ const packageCopyJson = exists('package.json', packageOut) ? parseJson(fs.readFileSync(path.join(packageOut, 'package.json'), 'utf8')) : null
98
+ const installedComparison = compareInstalled(manifest, installedRoot)
99
+ const installedCommand = installedRoot
100
+ ? run(process.execPath, [
101
+ path.join(installedRoot, 'scripts', 'run-gse-command.mjs'),
102
+ '--root',
103
+ installedRoot,
104
+ '--target',
105
+ installedRoot,
106
+ '--command',
107
+ '/gse maintenance --package-smoke --skip-release-bundle',
108
+ '--json',
109
+ '--compact',
110
+ ], installedRoot)
111
+ : null
112
+ const installedCommandData = installedCommand ? parseJson(installedCommand.stdout) : null
113
+
114
+ const requiredPackageFiles = [
115
+ 'SKILL.md',
116
+ 'package.json',
117
+ 'references/commands.md',
118
+ 'references/maintenance-cadence.md',
119
+ 'scripts/backfill-evidence-levels.mjs',
120
+ 'scripts/audit-evidence-review-queue.mjs',
121
+ 'scripts/audit-installed-sync.mjs',
122
+ 'scripts/audit-session-sync.mjs',
123
+ 'scripts/audit-maintenance-snapshot.mjs',
124
+ 'scripts/generate-maintenance-snapshot.mjs',
125
+ 'scripts/audit-maintenance-cadence.mjs',
126
+ 'scripts/record-session-sync.mjs',
127
+ 'scripts/run-gse-command.mjs',
128
+ 'scripts/validate-gse.mjs',
129
+ ]
130
+
131
+ const checks = [
132
+ check('IS01', 'package-gse creates a fresh package manifest', packageRun.status === 0 && manifest?.fileCount > 20 && Boolean(manifest?.integrity?.packageDigest), packageRun.command),
133
+ check('IS02', 'package manifest includes installed sync entrypoint and maintenance command support', manifest?.files?.includes('scripts/audit-installed-sync.mjs') && manifest?.files?.includes('scripts/audit-maintenance-cadence.mjs') && manifest?.files?.includes('references/maintenance-cadence.md'), 'gse-package-manifest.json'),
134
+ check('IS03', 'required package files are present in the package output', requiredPackageFiles.every((file) => exists(file, packageOut)), requiredPackageFiles.join(', ')),
135
+ check('IS04', 'package copy preserves package metadata version', packageJson?.version && packageCopyJson?.version === packageJson.version, `source=${packageJson?.version ?? 'missing'}, package=${packageCopyJson?.version ?? 'missing'}`),
136
+ check('IS05', 'package manifest does not expose the source root', manifest && !JSON.stringify(manifest).includes(root), 'gse-package-manifest.json'),
137
+ installedRoot
138
+ ? check('IS06', 'installed root exists and contains GSE entrypoint', exists('SKILL.md', installedRoot) && exists('scripts/run-gse-command.mjs', installedRoot), installedRoot)
139
+ : check('IS06', 'installed root comparison is optional and explicit', true, 'no --installed-root supplied; package self-check only'),
140
+ installedRoot
141
+ ? check('IS07', 'installed root matches fresh package file hashes', installedComparison.compared > 20 && installedComparison.missing.length === 0 && installedComparison.mismatched.length === 0, `compared=${installedComparison.compared}, missing=${installedComparison.missing.length}, mismatched=${installedComparison.mismatched.length}`, 'Run the install/sync step again before claiming the installed skill is fresh.')
142
+ : check('IS07', 'installed hash comparison skipped without installed root', true, 'supply --installed-root to compare a real installed copy'),
143
+ installedRoot
144
+ ? check('IS08', 'installed copy can run package-only /gse maintenance smoke', installedCommand?.status === 0 && installedCommandData?.summary?.failed === 0, installedCommand?.command ?? 'not run')
145
+ : check('IS08', 'installed command smoke skipped without installed root', true, 'supply --installed-root to run installed command smoke'),
146
+ ]
147
+
148
+ const passed = checks.filter((item) => item.status === 'passed').length
149
+ const failed = checks.length - passed
150
+ const report = {
151
+ root,
152
+ installedRoot,
153
+ generatedAt: new Date().toISOString(),
154
+ tempRoot,
155
+ summary: { status: failed === 0 ? 'passed' : 'failed', passed, failed, total: checks.length },
156
+ workflows: {
157
+ freshPackage: packageRun.status === 0 ? 'verified' : 'failed',
158
+ installedSync: installedRoot ? failed === 0 ? 'verified' : 'failed' : 'not-requested',
159
+ installedCommandSmoke: installedRoot ? installedCommand?.status === 0 ? 'verified' : 'failed' : 'not-requested',
160
+ },
161
+ comparison: installedComparison,
162
+ limits: [
163
+ 'Without --installed-root this audit verifies fresh package contents only.',
164
+ 'With --installed-root it compares every packaged file hash against the installed copy and runs a package-only /gse maintenance smoke from that installed copy.',
165
+ 'This audit does not publish a package, update npm, or prove native host slash-command support.',
166
+ ],
167
+ checks,
168
+ }
169
+
170
+ function renderMarkdown(data) {
171
+ const lines = []
172
+ lines.push('# GSE Installed Sync Audit')
173
+ lines.push('')
174
+ lines.push('Generated: ' + data.generatedAt)
175
+ lines.push('Root: ' + data.root)
176
+ lines.push('Installed root: ' + (data.installedRoot ?? 'not provided'))
177
+ lines.push('')
178
+ lines.push('## Summary')
179
+ lines.push('')
180
+ lines.push('- Status: ' + data.summary.status)
181
+ lines.push('- Checks: ' + data.summary.passed + '/' + data.summary.total)
182
+ lines.push('- Fresh package: ' + data.workflows.freshPackage)
183
+ lines.push('- Installed sync: ' + data.workflows.installedSync)
184
+ lines.push('')
185
+ lines.push('## Checks')
186
+ lines.push('')
187
+ for (const item of data.checks) {
188
+ const marker = item.status === 'passed' ? '[x]' : '[ ]'
189
+ lines.push('- ' + marker + ' ' + item.id + ' ' + item.label + ': ' + item.evidence)
190
+ }
191
+ lines.push('')
192
+ lines.push('## Limits')
193
+ lines.push('')
194
+ for (const item of data.limits) lines.push('- ' + item)
195
+ return lines.join('\n') + '\n'
196
+ }
197
+
198
+ if (jsonOnly) console.log(JSON.stringify(report, null, 2))
199
+ else console.log(renderMarkdown(report))
200
+
201
+ fs.rmSync(tempRoot, { recursive: true, force: true })
202
+
203
+ if (failed > 0) process.exit(1)
@@ -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
+ }