@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.
- package/.gse/state.json +147 -26
- package/CHANGELOG.md +19 -3
- package/README.md +13 -6
- package/README.zh-CN.md +13 -6
- package/SKILL.md +32 -12
- package/assets/templates/dispatch-packet.md +22 -14
- package/assets/templates/evidence.md +11 -5
- package/assets/templates/role-fallback-packet.md +49 -0
- package/package.json +18 -10
- package/references/agent-roles.md +41 -20
- package/references/commands.md +74 -45
- package/references/drift-audit.md +2 -1
- package/references/evidence-taxonomy.md +46 -9
- package/references/file-ownership.md +17 -13
- package/references/final-form-roadmap.md +201 -0
- package/references/final-readiness.md +3 -3
- package/references/host-adapters.md +38 -10
- package/references/learning-system.md +24 -8
- package/references/maintenance-cadence.md +51 -0
- package/references/project-guards.md +52 -0
- package/references/quality-gates.md +18 -11
- package/references/role-dispatch-fallback.md +54 -0
- package/references/tool-adapters.md +13 -3
- package/scripts/audit-close-gate-hardening.mjs +188 -0
- package/scripts/audit-close-gate.mjs +219 -33
- package/scripts/audit-command-execution.mjs +30 -17
- package/scripts/audit-commands.mjs +7 -5
- package/scripts/audit-completion-plan-drill.mjs +230 -0
- package/scripts/audit-completion-readiness.mjs +9 -6
- package/scripts/audit-continue-preflight.mjs +234 -0
- package/scripts/audit-evidence-levels.mjs +217 -0
- package/scripts/audit-evidence-review-queue.mjs +206 -0
- package/scripts/audit-final-acceptance-packet.mjs +9 -9
- package/scripts/audit-final-form-progress-report.mjs +19 -18
- package/scripts/audit-final-form-roadmap.mjs +157 -0
- package/scripts/audit-final-form-stale-copy.mjs +2 -2
- package/scripts/audit-final-readiness.mjs +3 -3
- package/scripts/audit-fresh-session-readiness.mjs +1 -1
- package/scripts/audit-host-capabilities.mjs +237 -0
- package/scripts/audit-installed-sync.mjs +203 -0
- package/scripts/audit-learning-drift.mjs +292 -0
- package/scripts/audit-learning-promotion.mjs +351 -0
- package/scripts/audit-learning-system.mjs +24 -14
- package/scripts/audit-local-final-form-completion.mjs +11 -10
- package/scripts/audit-maintenance-cadence.mjs +139 -0
- package/scripts/audit-maintenance-snapshot.mjs +181 -0
- package/scripts/audit-npm-package-metadata.mjs +8 -2
- package/scripts/audit-npm-publish-dry-run.mjs +10 -4
- package/scripts/audit-npm-tarball-install.mjs +9 -3
- package/scripts/audit-owner-external-gate-kit.mjs +12 -11
- package/scripts/audit-project-guards.mjs +189 -0
- package/scripts/audit-project.mjs +14 -11
- package/scripts/audit-public-acceptance-handoff.mjs +10 -10
- package/scripts/audit-public-acceptance-readiness.mjs +6 -6
- package/scripts/audit-public-release-checklist.mjs +17 -15
- package/scripts/audit-release-bundle.mjs +13 -11
- package/scripts/audit-release-owner-action-plan-drill.mjs +5 -4
- package/scripts/audit-release-owner-action-plan.mjs +10 -10
- package/scripts/audit-release-status-manifest.mjs +4 -4
- package/scripts/audit-roadmap-consistency.mjs +11 -7
- package/scripts/audit-role-dispatch-fallback.mjs +212 -0
- package/scripts/audit-session-sync.mjs +127 -0
- package/scripts/audit-state-freshness.mjs +6 -5
- package/scripts/audit-state-repair.mjs +360 -0
- package/scripts/audit-target-hardening-drills.mjs +267 -0
- package/scripts/audit-tool-fallback-policy.mjs +180 -0
- package/scripts/audit-ui-browser-evidence-policy.mjs +191 -0
- package/scripts/audit-validation-profiles.mjs +6 -4
- package/scripts/backfill-evidence-levels.mjs +98 -0
- package/scripts/check-encoding.mjs +72 -0
- package/scripts/generate-continue-packet.mjs +1214 -0
- package/scripts/generate-final-acceptance-packet.mjs +23 -8
- package/scripts/generate-final-form-progress-report.mjs +25 -23
- package/scripts/generate-host-runtime-evidence-handoff.mjs +23 -16
- package/scripts/generate-maintenance-snapshot.mjs +216 -0
- package/scripts/generate-public-acceptance-handoff.mjs +26 -14
- package/scripts/generate-release-owner-action-plan.mjs +4 -3
- package/scripts/generate-release-status-manifest.mjs +4 -3
- package/scripts/init-project.mjs +144 -63
- package/scripts/record-learning.mjs +37 -8
- package/scripts/record-session-sync.mjs +87 -0
- package/scripts/run-gse-command.mjs +79 -47
- package/scripts/run-validation-profile.mjs +24 -5
- 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
|
+
}
|