@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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +42 -42
- package/.github/ISSUE_TEMPLATE/change_request.yml +50 -50
- package/.github/ISSUE_TEMPLATE/config.yml +5 -5
- package/.github/PULL_REQUEST_TEMPLATE.md +38 -38
- package/.github/workflows/validate-gse.yml +33 -33
- package/.gse/README.md +18 -18
- package/.gse/gse-development-protocol.md +50 -50
- package/.gse/project-profile.md +29 -29
- package/.gse/quality-gates.md +25 -25
- package/.gse/releases/public-registry-publication-npm.md +49 -0
- package/.gse/releases/public-release-owner-required.md +65 -65
- package/.gse/releases/public-security-contact-owner-required.md +45 -45
- package/.gse/state.json +147 -27
- package/CHANGELOG.md +31 -15
- package/CONTRIBUTING.md +64 -64
- package/LICENSE +21 -21
- package/README.md +14 -7
- package/README.zh-CN.md +14 -7
- package/SECURITY.md +41 -41
- package/SKILL.md +32 -12
- package/SUPPORT.md +38 -38
- package/assets/marketplace/README.md +7 -7
- package/assets/marketplace/gse-listing.json +75 -75
- package/assets/templates/acceptance-execution-packet.md +73 -73
- package/assets/templates/adr.md +14 -14
- package/assets/templates/change-brief.md +16 -16
- package/assets/templates/design.md +18 -18
- package/assets/templates/dispatch-packet.md +100 -92
- package/assets/templates/evidence.md +15 -9
- package/assets/templates/execution-quality-pack.md +65 -65
- package/assets/templates/goal-map.md +21 -21
- package/assets/templates/host-adapter.md +48 -48
- package/assets/templates/host-ui-invocation-record.md +42 -42
- package/assets/templates/incident-review.md +60 -60
- package/assets/templates/public-channel-publication-record.md +49 -49
- package/assets/templates/public-ci-run-record.md +53 -53
- package/assets/templates/public-release-record.md +65 -65
- package/assets/templates/public-repository-settings-record.md +59 -59
- package/assets/templates/public-security-contact-record.md +45 -45
- package/assets/templates/release-trust-record.md +32 -32
- package/assets/templates/review.md +18 -18
- package/assets/templates/role-fallback-packet.md +49 -0
- package/assets/templates/spec.md +16 -16
- package/assets/templates/target-adoption-evidence.md +29 -29
- package/assets/templates/tasks.md +17 -17
- package/assets/templates/update-release-acceptance-record.md +58 -58
- package/examples/README.md +22 -22
- package/examples/agent-runtime-host/.claude/gse-adapter.md +6 -6
- package/examples/agent-runtime-host/.codex/gse-adapter.md +7 -7
- package/examples/agent-runtime-host/.gse/goal-map.md +7 -7
- package/examples/agent-runtime-host/.gse/project-profile.md +27 -27
- package/examples/agent-runtime-host/.gse/tooling.md +5 -5
- package/examples/agent-runtime-host/.mcp.json +9 -9
- package/examples/agent-runtime-host/AGENTS.md +5 -5
- package/examples/agent-runtime-host/README.md +10 -10
- package/examples/agent-runtime-host/docs/model-routing.md +5 -5
- package/examples/cli-tool/.gse/project-profile.md +21 -21
- package/examples/cli-tool/AGENTS.md +5 -5
- package/examples/cli-tool/README.md +12 -12
- package/examples/cli-tool/package.json +21 -21
- package/examples/small-app/.env.example +2 -2
- package/examples/small-app/.github/workflows/ci.yml +8 -8
- package/examples/small-app/AGENTS.md +5 -5
- package/examples/small-app/README.md +12 -12
- package/examples/small-app/package.json +26 -26
- package/examples/small-app/playwright.config.ts +4 -4
- package/package.json +52 -44
- package/references/adoption-recipes.md +171 -171
- package/references/agent-roles.md +42 -21
- package/references/architecture-health.md +101 -101
- package/references/benchmark-audit.md +73 -73
- package/references/commands.md +74 -45
- package/references/community-channels.md +46 -46
- package/references/compatibility.md +70 -70
- package/references/design-basis.md +38 -38
- package/references/domain-model.md +129 -129
- package/references/domain-quality-gates.md +75 -75
- package/references/drift-audit.md +81 -80
- package/references/evidence-taxonomy.md +145 -108
- package/references/file-ownership.md +97 -93
- package/references/final-form-roadmap.md +201 -0
- package/references/final-readiness.md +84 -84
- package/references/forward-test.md +133 -133
- package/references/goal-map.md +36 -36
- package/references/host-adapters.md +129 -101
- package/references/learning-system.md +42 -26
- package/references/maintenance-cadence.md +51 -0
- package/references/marketplace-discovery.md +46 -46
- package/references/model-routing.md +103 -103
- package/references/open-source-defaults.md +39 -39
- package/references/operating-model.md +52 -52
- package/references/packaging.md +277 -277
- package/references/project-agent-workspace.md +89 -89
- package/references/project-bootstrap.md +122 -122
- package/references/project-guards.md +52 -0
- package/references/project-profile.md +57 -57
- package/references/public-release.md +174 -174
- package/references/quality-gates.md +96 -89
- package/references/recovery.md +176 -176
- package/references/release-trust.md +43 -43
- package/references/release.md +126 -126
- package/references/review.md +141 -141
- package/references/role-dispatch-fallback.md +54 -0
- package/references/router.md +90 -90
- package/references/spec-workflow.md +66 -66
- package/references/task-levels.md +81 -81
- package/references/tool-adapters.md +80 -70
- package/scripts/audit-acceptance-execution-packet.mjs +133 -133
- package/scripts/audit-adoption-recipes.mjs +99 -99
- package/scripts/audit-change-lifecycle.mjs +77 -77
- package/scripts/audit-change-system.mjs +134 -134
- package/scripts/audit-ci-readiness.mjs +107 -107
- package/scripts/audit-close-gate-hardening.mjs +188 -0
- package/scripts/audit-close-gate.mjs +448 -262
- package/scripts/audit-command-adapters.mjs +91 -91
- package/scripts/audit-command-execution.mjs +212 -199
- package/scripts/audit-commands.mjs +7 -5
- package/scripts/audit-compatibility.mjs +149 -149
- package/scripts/audit-completion-plan-drill.mjs +230 -0
- package/scripts/audit-completion-readiness.mjs +290 -287
- package/scripts/audit-continue-preflight.mjs +234 -0
- package/scripts/audit-distribution.mjs +251 -251
- package/scripts/audit-domain-quality-gates.mjs +108 -108
- package/scripts/audit-evidence-levels.mjs +217 -0
- package/scripts/audit-evidence-placeholders.mjs +126 -126
- 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-promotion.mjs +255 -255
- package/scripts/audit-final-readiness.mjs +226 -226
- package/scripts/audit-fixtures.mjs +154 -154
- package/scripts/audit-fresh-session-readiness.mjs +177 -177
- package/scripts/audit-host-capabilities.mjs +237 -0
- package/scripts/audit-host-runtime-evidence-handoff.mjs +159 -159
- package/scripts/audit-host-runtime-invocation-drill.mjs +240 -240
- package/scripts/audit-host-runtime-invocations.mjs +254 -254
- package/scripts/audit-host-ui-invocation.mjs +132 -132
- 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-marketplace-discovery.mjs +122 -122
- package/scripts/audit-npm-package-metadata.mjs +160 -154
- package/scripts/audit-npm-publish-dry-run.mjs +194 -166
- package/scripts/audit-npm-tarball-install.mjs +195 -189
- package/scripts/audit-open-source-defaults.mjs +92 -92
- package/scripts/audit-open-source-readiness.mjs +97 -97
- package/scripts/audit-owner-external-gate-kit.mjs +12 -11
- package/scripts/audit-project-guards.mjs +189 -0
- package/scripts/audit-project.mjs +144 -141
- package/scripts/audit-public-acceptance-command-dry-run-drill.mjs +203 -203
- package/scripts/audit-public-acceptance-handoff.mjs +10 -10
- package/scripts/audit-public-acceptance-readiness.mjs +222 -222
- package/scripts/audit-public-channel-publication.mjs +248 -248
- package/scripts/audit-public-ci-run.mjs +184 -184
- package/scripts/audit-public-collaboration-templates.mjs +98 -98
- package/scripts/audit-public-external-gate-probe.mjs +206 -206
- package/scripts/audit-public-release-checklist.mjs +17 -15
- package/scripts/audit-public-release-decision.mjs +201 -201
- package/scripts/audit-public-release-metadata.mjs +176 -176
- package/scripts/audit-public-repository-settings.mjs +237 -237
- package/scripts/audit-public-security-contact.mjs +171 -171
- package/scripts/audit-readme-docs.mjs +6 -4
- package/scripts/audit-recovery-readiness.mjs +98 -98
- 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-readiness.mjs +106 -106
- package/scripts/audit-release-status-manifest.mjs +4 -4
- package/scripts/audit-release-trust.mjs +62 -62
- package/scripts/audit-remote-distribution.mjs +266 -266
- package/scripts/audit-roadmap-consistency.mjs +234 -230
- package/scripts/audit-role-dispatch-fallback.mjs +212 -0
- package/scripts/audit-session-sync.mjs +127 -0
- package/scripts/audit-signing.mjs +147 -147
- package/scripts/audit-state-freshness.mjs +20 -14
- package/scripts/audit-state-repair.mjs +360 -0
- package/scripts/audit-target-adoption-evidence.mjs +117 -117
- package/scripts/audit-target-hardening-drills.mjs +267 -0
- package/scripts/audit-target-project.mjs +507 -507
- package/scripts/audit-tool-fallback-policy.mjs +180 -0
- package/scripts/audit-ui-browser-evidence-policy.mjs +191 -0
- package/scripts/audit-update-release-acceptance.mjs +136 -136
- package/scripts/audit-v1-target-validation.mjs +269 -269
- package/scripts/audit-validation-profiles.mjs +125 -123
- package/scripts/backfill-evidence-levels.mjs +98 -0
- package/scripts/check-encoding.mjs +72 -0
- package/scripts/close-change.mjs +116 -116
- package/scripts/discover-project-profile.mjs +307 -307
- package/scripts/generate-command-adapter.mjs +231 -231
- package/scripts/generate-continue-packet.mjs +1214 -0
- package/scripts/generate-final-acceptance-packet.mjs +188 -173
- package/scripts/generate-final-form-progress-report.mjs +191 -189
- package/scripts/generate-host-runtime-evidence-handoff.mjs +198 -191
- package/scripts/generate-maintenance-snapshot.mjs +216 -0
- package/scripts/generate-owner-external-gate-kit.mjs +295 -295
- package/scripts/generate-public-acceptance-handoff.mjs +168 -156
- package/scripts/generate-public-release-checklist.mjs +207 -207
- package/scripts/generate-release-bundle.mjs +505 -505
- package/scripts/generate-release-owner-action-plan.mjs +172 -171
- package/scripts/generate-release-status-manifest.mjs +199 -198
- package/scripts/generate-session-prompt.mjs +188 -188
- package/scripts/gse.mjs +67 -67
- package/scripts/init-change.mjs +265 -265
- package/scripts/init-project.mjs +793 -712
- package/scripts/install-gse.mjs +234 -234
- package/scripts/lib/evidence-placeholders.mjs +28 -28
- package/scripts/package-gse.mjs +174 -174
- package/scripts/probe-public-external-gates.mjs +167 -167
- package/scripts/record-host-invocation.mjs +151 -151
- package/scripts/record-learning.mjs +37 -8
- package/scripts/record-public-channel-publication.mjs +178 -178
- package/scripts/record-public-ci-run.mjs +180 -180
- package/scripts/record-public-release.mjs +175 -175
- package/scripts/record-public-repository-settings.mjs +209 -209
- package/scripts/record-public-security-contact.mjs +157 -157
- 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/sign-gse-package.mjs +83 -83
- package/scripts/update-project-state.mjs +223 -223
- package/scripts/validate-gse.mjs +216 -0
- 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
|
+
}
|