@t275005746/gse 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -91,7 +91,232 @@ function evidenceFromRun(runResult) {
91
91
  return parts.join(', ') || `exit:${runResult.status}`
92
92
  }
93
93
 
94
- function createFixture(label) {
94
+ function compactText(value, limit = 180) {
95
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim()
96
+ if (text.length <= limit) return text
97
+ return text.slice(0, limit - 3) + '...'
98
+ }
99
+
100
+ function warningChecks(runResult) {
101
+ return (runResult.parsed?.checks ?? [])
102
+ .filter((item) => item.status === 'warning' || item.status === 'failed')
103
+ .map((item) => ({
104
+ id: item.id,
105
+ label: item.label,
106
+ status: item.status,
107
+ evidence: compactText(item.evidence),
108
+ recommendation: compactText(item.recommendation),
109
+ }))
110
+ }
111
+
112
+ function repairIssues(runResult) {
113
+ return (runResult.parsed?.repairActions ?? [])
114
+ .map((item) => ({
115
+ id: item.id,
116
+ label: item.problem,
117
+ status: item.severity === 'hard' ? 'failed' : 'warning',
118
+ evidence: item.targetPath,
119
+ recommendation: compactText(item.command),
120
+ }))
121
+ }
122
+
123
+ function uniqueIssues(items, limit = 8) {
124
+ const seen = new Set()
125
+ const result = []
126
+ for (const item of items) {
127
+ const key = `${item.id}:${item.label}:${item.evidence}`
128
+ if (seen.has(key)) continue
129
+ seen.add(key)
130
+ result.push(item)
131
+ if (result.length >= limit) break
132
+ }
133
+ return result
134
+ }
135
+
136
+ function classifyTargetAdoption({ hasHardFailure, hasWarnings, topLocalIssues }) {
137
+ if (hasHardFailure) return 'target-hard-failure'
138
+ if (topLocalIssues.length > 0) return 'target-local-adoption-hygiene'
139
+ if (hasWarnings) return 'gse-ready-with-soft-warnings'
140
+ return 'gse-ready'
141
+ }
142
+
143
+ function hasDirtyGseIssue(topLocalIssues) {
144
+ return topLocalIssues.some((item) => {
145
+ const id = String(item.id ?? '')
146
+ const evidence = String(item.evidence ?? '').toLowerCase()
147
+ if (id.startsWith('SR')) return false
148
+ return (
149
+ id === 'TPD09' ||
150
+ id === 'CG08' ||
151
+ evidence.includes('.gse/') ||
152
+ evidence.includes('.gse\\')
153
+ )
154
+ })
155
+ }
156
+
157
+ function hasDirtyTargetWorktreeIssue(topLocalIssues) {
158
+ return topLocalIssues.some((item) => {
159
+ const id = String(item.id ?? '')
160
+ const label = String(item.label ?? '').toLowerCase()
161
+ const evidence = String(item.evidence ?? '').toLowerCase()
162
+ return (
163
+ id === 'CG11' ||
164
+ label.includes('worktree') ||
165
+ evidence.includes('staged') ||
166
+ evidence.includes('unstaged') ||
167
+ evidence.includes('untracked') ||
168
+ evidence.includes('conflict')
169
+ )
170
+ })
171
+ }
172
+
173
+ function hasStateRepairIssue(topLocalIssues) {
174
+ return topLocalIssues.some((item) => String(item.id ?? '').startsWith('SR'))
175
+ }
176
+
177
+ function buildRepairPlan({ topLocalIssues, stateRepairSummary }) {
178
+ const dirtyGse = hasDirtyGseIssue(topLocalIssues)
179
+ const dirtyTargetWorktree = hasDirtyTargetWorktreeIssue(topLocalIssues)
180
+ const stateRepairAdvised = hasStateRepairIssue(topLocalIssues) || (stateRepairSummary.actions ?? 0) > 0
181
+ const repairBlockedByDirtyWorktree = dirtyGse || dirtyTargetWorktree
182
+ const steps = []
183
+ if (dirtyTargetWorktree) {
184
+ steps.push({
185
+ id: 'resolve-worktree-ownership',
186
+ status: 'required-first',
187
+ action: 'Review current target worktree ownership, finish or exclude unrelated target-session changes, and stage/commit only the active slice before repair.',
188
+ })
189
+ } else if (dirtyGse) {
190
+ steps.push({
191
+ id: 'resolve-gse-worktree-ownership',
192
+ status: 'required-first',
193
+ action: 'Review current .gse worktree ownership, finish or exclude unrelated target-session changes, and stage/commit only the active slice before repair.',
194
+ })
195
+ }
196
+ if (stateRepairAdvised) {
197
+ steps.push({
198
+ id: 'compact-state-risks',
199
+ status: repairBlockedByDirtyWorktree ? 'blocked-until-worktree-owned' : 'ready',
200
+ action: 'Run `/gse repair --execute` only after the target worktree and .gse state are owned and reversible backup output is acceptable.',
201
+ })
202
+ }
203
+ if (!steps.length) {
204
+ steps.push({
205
+ id: 'no-repair-required',
206
+ status: 'ready',
207
+ action: 'No target adoption repair step is required before normal continuation.',
208
+ })
209
+ }
210
+ return {
211
+ repairBlockedByDirtyGse: dirtyGse && stateRepairAdvised,
212
+ repairBlockedByDirtyWorktree: repairBlockedByDirtyWorktree && stateRepairAdvised,
213
+ stateRepairAdvised,
214
+ dirtyGseWorktree: dirtyGse,
215
+ dirtyTargetWorktree,
216
+ steps,
217
+ }
218
+ }
219
+
220
+ function buildRecommendedNextActions(topLocalIssues, repairPlan) {
221
+ const actions = []
222
+ for (const step of repairPlan.steps) {
223
+ if (step.id === 'no-repair-required') continue
224
+ actions.push(step.action)
225
+ }
226
+ for (const issue of uniqueIssues(topLocalIssues, 8)) {
227
+ if (!issue.recommendation) continue
228
+ if (repairPlan.repairBlockedByDirtyWorktree && String(issue.id ?? '').startsWith('SR')) continue
229
+ actions.push(issue.recommendation)
230
+ }
231
+ return [...new Set(actions)].slice(0, 6)
232
+ }
233
+
234
+ function buildTargetAdoptionSummary(commandRuns, checks) {
235
+ const failed = checks.filter((item) => item.status === 'failed')
236
+ const warnings = checks.filter((item) => item.status === 'warning')
237
+ const continueSummary = commandRuns.continue.parsed?.summary ?? {}
238
+ const stateRepairSummary = commandRuns.stateRepair.parsed?.summary ?? {}
239
+ const portableContinueUsable =
240
+ commandRuns.continue.status === 0 &&
241
+ (continueSummary.failedHardChecks ?? 0) === 0 &&
242
+ (continueSummary.blockedGates ?? 0) === 0
243
+ const activeRiskCount = continueSummary.activeRiskCount ?? continueSummary.riskCount ?? null
244
+ const topLocalIssues = uniqueIssues([
245
+ ...repairIssues(commandRuns.stateRepair),
246
+ ...warningChecks(commandRuns.doctor),
247
+ ...warningChecks(commandRuns.close),
248
+ ...warningChecks(commandRuns.hostCapabilities),
249
+ ...warningChecks(commandRuns.learningDrift),
250
+ ])
251
+ const repairPlan = buildRepairPlan({ topLocalIssues, stateRepairSummary })
252
+ const recommendedNextActions = buildRecommendedNextActions(topLocalIssues, repairPlan)
253
+ const hasHardFailure = failed.length > 0 || (stateRepairSummary.hard ?? 0) > 0
254
+ const hasWarnings = warnings.length > 0 || (stateRepairSummary.warnings ?? 0) > 0
255
+ return {
256
+ classification: classifyTargetAdoption({ hasHardFailure, hasWarnings, topLocalIssues }),
257
+ gseCoreGap: false,
258
+ coreGapAssessment: 'not-assessed-by-target-drill',
259
+ portableContinueUsable,
260
+ hostNativeSlashCommand: 'not-proven',
261
+ longPromptRisk: portableContinueUsable
262
+ ? Number.isFinite(activeRiskCount) && activeRiskCount > 20
263
+ ? 'risk-dump-needs-compaction'
264
+ : 'low'
265
+ : 'unknown',
266
+ activeRiskCount,
267
+ stateRepairStatus: stateRepairSummary.status ?? 'unknown',
268
+ topLocalIssues,
269
+ repairPlan,
270
+ recommendedNextActions,
271
+ limits: [
272
+ 'This classifies target-project adoption hygiene, not product readiness.',
273
+ 'gseCoreGap=false means this target drill did not find or claim a GSE core gap; full core-gap coverage belongs to GSE self audits.',
274
+ 'Portable /gse continue success does not prove host-native slash-command support.',
275
+ 'Warnings should be fixed before claiming a target project is fully GSE-ready, but they do not block normal continuation unless project policy says so.',
276
+ ],
277
+ }
278
+ }
279
+
280
+ function validateAdoptionSummary(target) {
281
+ const summary = target.adoptionSummary
282
+ const allowedClassifications = new Set([
283
+ 'gse-ready',
284
+ 'gse-ready-with-soft-warnings',
285
+ 'target-local-adoption-hygiene',
286
+ 'target-hard-failure',
287
+ ])
288
+ const failures = []
289
+ if (!summary || typeof summary !== 'object') failures.push('missing adoptionSummary')
290
+ if (summary && !allowedClassifications.has(summary.classification)) failures.push('invalid classification')
291
+ if (summary && typeof summary.gseCoreGap !== 'boolean') failures.push('gseCoreGap is not boolean')
292
+ if (summary && summary.coreGapAssessment !== 'not-assessed-by-target-drill') failures.push('coreGapAssessment boundary is missing')
293
+ if (summary && typeof summary.portableContinueUsable !== 'boolean') failures.push('portableContinueUsable is not boolean')
294
+ if (summary && !Array.isArray(summary.topLocalIssues)) failures.push('topLocalIssues is not an array')
295
+ if (summary && !summary.repairPlan) failures.push('missing repairPlan')
296
+ if (summary?.repairPlan && typeof summary.repairPlan.repairBlockedByDirtyGse !== 'boolean') failures.push('repairPlan.repairBlockedByDirtyGse is not boolean')
297
+ if (summary?.repairPlan && typeof summary.repairPlan.repairBlockedByDirtyWorktree !== 'boolean') failures.push('repairPlan.repairBlockedByDirtyWorktree is not boolean')
298
+ if (summary?.repairPlan && typeof summary.repairPlan.dirtyTargetWorktree !== 'boolean') failures.push('repairPlan.dirtyTargetWorktree is not boolean')
299
+ if (summary?.repairPlan && !Array.isArray(summary.repairPlan.steps)) failures.push('repairPlan.steps is not an array')
300
+ if (summary && !Array.isArray(summary.recommendedNextActions)) failures.push('recommendedNextActions is not an array')
301
+ if (summary?.repairPlan?.repairBlockedByDirtyWorktree) {
302
+ const firstAction = String(summary.recommendedNextActions?.[0] ?? '')
303
+ if (!firstAction.includes('worktree ownership')) failures.push('dirty repair plan does not prioritize worktree ownership')
304
+ }
305
+ if (summary?.classification === 'target-local-adoption-hygiene' && summary.topLocalIssues.length === 0) {
306
+ failures.push('local hygiene classification has no local issues')
307
+ }
308
+ return check(
309
+ `${target.id}-adoption-summary`,
310
+ 'target adoption summary is structured and separates soft warnings from local hygiene',
311
+ failures.length === 0 ? 'passed' : 'failed',
312
+ failures.length === 0
313
+ ? `classification:${summary.classification}, portableContinueUsable:${summary.portableContinueUsable}, gseCoreGap:${summary.gseCoreGap}`
314
+ : failures.join('; '),
315
+ 'Keep adoptionSummary machine-readable so project adoption reports do not overstate GSE core gaps.',
316
+ )
317
+ }
318
+
319
+ function createFixture(label, options = {}) {
95
320
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gse-hardening-${label}-`))
96
321
  const init = run('init-project.mjs', ['--target', dir, '--mode', 'enterprise', '--json'])
97
322
  fs.mkdirSync(path.join(dir, 'docs'), { recursive: true })
@@ -108,7 +333,9 @@ function createFixture(label) {
108
333
  nextAction: 'Continue target hardening drill.',
109
334
  }
110
335
  state.lastEvidence = '.gse/evidence/2026-07-09.md'
111
- state.residualRisks = []
336
+ state.residualRisks = options.committedRepair
337
+ ? Array.from({ length: 8 }, (_, index) => `Committed fixture residual risk ${index + 1} that should be compacted only after worktree ownership is resolved.`)
338
+ : []
112
339
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8')
113
340
  fs.appendFileSync(path.join(dir, '.gse', 'README.md'), '\nCanonical plan: `docs/productization-architecture.md`.\n', 'utf8')
114
341
  fs.writeFileSync(path.join(dir, '.gse', 'evidence', '2026-07-09.md'), '# Evidence\n\nFixture hardening evidence.\n', 'utf8')
@@ -128,6 +355,14 @@ function createFixture(label) {
128
355
  spawnSync('git', ['config', 'user.name', 'GSE Fixture'], { cwd: dir, encoding: 'utf8', windowsHide: true })
129
356
  spawnSync('git', ['add', '.'], { cwd: dir, encoding: 'utf8', windowsHide: true })
130
357
  spawnSync('git', ['commit', '-m', 'fixture'], { cwd: dir, encoding: 'utf8', windowsHide: true })
358
+ if (options.dirtyRepair || options.dirtyProductRepair) {
359
+ state.residualRisks = Array.from({ length: 8 }, (_, index) => `Fixture residual risk ${index + 1} that should be compacted after worktree ownership is resolved.`)
360
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8')
361
+ }
362
+ if (options.dirtyProductRepair) {
363
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true })
364
+ fs.writeFileSync(path.join(dir, 'src', 'active-slice.js'), 'export const activeSlice = true\n', 'utf8')
365
+ }
131
366
  return { dir, init }
132
367
  }
133
368
 
@@ -138,6 +373,7 @@ function auditTarget(target) {
138
373
  close: run('audit-close-gate.mjs', ['--target', target.root, '--json']),
139
374
  hostCapabilities: run('audit-host-capabilities.mjs', ['--root', root, '--target', target.root, '--json']),
140
375
  learningDrift: run('audit-learning-drift.mjs', ['--root', root, '--target', target.root, '--json']),
376
+ stateRepair: run('audit-state-repair.mjs', ['--root', root, '--target', target.root, '--json']),
141
377
  }
142
378
  const checks = [
143
379
  check(`${target.id}-doctor`, 'target project doctor has no hard failures', statusFromRun(commandRuns.doctor), evidenceFromRun(commandRuns.doctor), 'Repair target .gse adoption drift before claiming GSE-ready status.'),
@@ -146,12 +382,15 @@ function auditTarget(target) {
146
382
  check(`${target.id}-host`, 'host capability records are audited with claim boundaries', statusFromRun(commandRuns.hostCapabilities), evidenceFromRun(commandRuns.hostCapabilities), 'Record missing host capability facts or keep unknown/external-required boundaries explicit.'),
147
383
  check(`${target.id}-learning`, 'learning drift is audited for promoted guard candidates', statusFromRun(commandRuns.learningDrift), evidenceFromRun(commandRuns.learningDrift), 'Promote high-severity learning drift into guards, gates, or scripts before close.'),
148
384
  ]
385
+ const adoptionSummary = buildTargetAdoptionSummary(commandRuns, checks)
386
+ checks.push(validateAdoptionSummary({ id: target.id, adoptionSummary }))
149
387
  const failed = checks.filter((item) => item.status === 'failed')
150
388
  const warnings = checks.filter((item) => item.status === 'warning')
151
389
  return {
152
390
  id: target.id,
153
391
  root: target.root,
154
392
  status: failed.length > 0 ? 'failed' : warnings.length > 0 ? 'warning' : 'passed',
393
+ adoptionSummary,
155
394
  checks,
156
395
  commands: Object.values(commandRuns).map((item) => item.command),
157
396
  summaries: Object.fromEntries(Object.entries(commandRuns).map(([key, value]) => [key, value.parsed?.summary ?? { exit: value.status }])),
@@ -169,15 +408,23 @@ function runConfiguredTargets() {
169
408
 
170
409
  function runSelfTestTargets() {
171
410
  const primary = createFixture('primary')
172
- const secondary = createFixture('secondary')
411
+ const secondary = createFixture('secondary', { dirtyRepair: true })
412
+ const dirtyProduct = createFixture('dirty-product', { dirtyProductRepair: true })
413
+ const committedRepairDirtyProduct = createFixture('committed-repair-dirty-product', { committedRepair: true })
414
+ fs.mkdirSync(path.join(committedRepairDirtyProduct.dir, 'src'), { recursive: true })
415
+ fs.writeFileSync(path.join(committedRepairDirtyProduct.dir, 'src', 'active-slice.js'), 'export const activeSlice = true\n', 'utf8')
173
416
  return {
174
417
  targets: [
175
418
  { id: 'fixture-primary', root: primary.dir },
176
419
  { id: 'fixture-secondary', root: secondary.dir },
420
+ { id: 'fixture-dirty-product', root: dirtyProduct.dir },
421
+ { id: 'fixture-committed-repair-dirty-product', root: committedRepairDirtyProduct.dir },
177
422
  ],
178
423
  cleanup: () => {
179
424
  fs.rmSync(primary.dir, { recursive: true, force: true })
180
425
  fs.rmSync(secondary.dir, { recursive: true, force: true })
426
+ fs.rmSync(dirtyProduct.dir, { recursive: true, force: true })
427
+ fs.rmSync(committedRepairDirtyProduct.dir, { recursive: true, force: true })
181
428
  },
182
429
  }
183
430
  }
@@ -212,6 +459,7 @@ function buildReport() {
212
459
  closeGateHardening: hardFailures.length === 0 ? 'verified' : 'failed',
213
460
  hostCapabilityBoundaries: hardFailures.length === 0 ? 'verified' : 'failed',
214
461
  learningDriftCoverage: hardFailures.length === 0 ? 'verified' : 'failed',
462
+ targetAdoptionHygieneSummary: hardFailures.length === 0 ? 'verified' : 'failed',
215
463
  },
216
464
  targets: reports,
217
465
  checks,
@@ -245,6 +493,9 @@ function renderMarkdown(report) {
245
493
  lines.push('')
246
494
  for (const target of report.targets) {
247
495
  lines.push('- ' + target.id + ': ' + target.status + ' at ' + target.root)
496
+ lines.push(' - Classification: ' + target.adoptionSummary.classification)
497
+ lines.push(' - Portable continue usable: ' + target.adoptionSummary.portableContinueUsable)
498
+ lines.push(' - Long prompt risk: ' + target.adoptionSummary.longPromptRisk)
248
499
  }
249
500
  lines.push('')
250
501
  lines.push('## Checks')
@@ -342,7 +342,7 @@ function buildCompletionPlan(target, state, maintenanceSnapshot) {
342
342
  id: 'encoding',
343
343
  active: docsOrLearningChanged && hasEncodingCheck,
344
344
  when: 'docs, markdown, JSON/JSONL, YAML, evidence, or learning files changed and package.json exposes check:encoding',
345
- command: 'cmd /c pnpm check:encoding',
345
+ command: 'cmd /c npm run check:encoding',
346
346
  },
347
347
  {
348
348
  id: 'installed-sync',
@@ -454,6 +454,8 @@ function buildShortPrompt({ projectName, resolvedTarget, preflightStatus, compac
454
454
  ]
455
455
  if (blockedGates.length) lines.push(`Claim evidence: ${blockedGates.map((gate) => gate.area).join(', ')}`)
456
456
  if (failedHardChecks.length) lines.push(`Fix first: ${failedHardChecks.map((item) => item.label).join('; ')}`)
457
+ if (compactState.productOutcomeGate?.status === 'warning') lines.push(`Product outcome: ${compactState.productOutcomeGate.recommendation}`)
458
+ if (compactState.productProgressDrift?.status === 'warning') lines.push(`Product drift: ${compactState.productProgressDrift.recommendation}`)
457
459
  lines.push('Do: one verifiable slice -> evidence -> state/goal-map/current-slice -> completionPlan checks -> commit.')
458
460
  return lines.join('\n')
459
461
  }
@@ -510,6 +512,216 @@ function isHostNativeOnlyCandidate(text) {
510
512
  )
511
513
  }
512
514
 
515
+ const internalProgressTerms = [
516
+ 'provenance',
517
+ 'boundary',
518
+ 'handoff',
519
+ 'lineage',
520
+ 'normalizer',
521
+ 'normalize',
522
+ 'source key',
523
+ 'state repair',
524
+ 'component',
525
+ 'store',
526
+ 'evidence',
527
+ ]
528
+
529
+ const visibleProgressTerms = [
530
+ 'browser',
531
+ 'user interface',
532
+ 'interface',
533
+ 'user-visible',
534
+ 'user visible',
535
+ 'visible',
536
+ 'canvas',
537
+ 'workflow',
538
+ 'execute',
539
+ 'execution',
540
+ 'provider',
541
+ 'api',
542
+ 'export',
543
+ 'import',
544
+ 'package',
545
+ 'result',
546
+ 'screenshot',
547
+ ]
548
+
549
+ const productProjectTerms = [
550
+ 'product',
551
+ 'productization',
552
+ 'user',
553
+ 'customer',
554
+ 'workflow',
555
+ 'interface',
556
+ 'canvas',
557
+ 'provider',
558
+ 'export',
559
+ 'browser',
560
+ ]
561
+
562
+ const skillProjectTerms = [
563
+ 'skill',
564
+ 'agentic engineering skill',
565
+ 'operating model',
566
+ 'open-source skill',
567
+ 'portable command',
568
+ 'host adapter',
569
+ ]
570
+
571
+ function hasAnyTerm(text, terms) {
572
+ const value = String(text || '').toLowerCase()
573
+ return terms.some((term) => value.includes(term))
574
+ }
575
+
576
+ function isInternalEvidenceRecord(record) {
577
+ const evidenceLevel = String(record?.evidenceLevel || '').toLowerCase()
578
+ const requiredEvidenceLevel = String(record?.requiredEvidenceLevel || '').toLowerCase()
579
+ const text = [
580
+ record?.summary,
581
+ record?.nextAction,
582
+ Array.isArray(record?.commands) ? record.commands.join(' ') : '',
583
+ ].join(' ')
584
+ const lowLevel = evidenceLevel === 'verified-unit' || evidenceLevel === 'verified-component'
585
+ const notVisibleProof = requiredEvidenceLevel !== 'verified-browser' && !hasAnyTerm(text, ['browser smoke', 'screenshot', 'playwright'])
586
+ return lowLevel && notVisibleProof && hasAnyTerm(text, internalProgressTerms)
587
+ }
588
+
589
+ function analyzeProductProgressDrift(records, state) {
590
+ const recent = Array.isArray(records) ? records.slice(-6) : []
591
+ const internalRecent = recent.filter(isInternalEvidenceRecord)
592
+ const currentText = [
593
+ state?.currentSlice?.outcome,
594
+ state?.currentSlice?.nextAction,
595
+ state?.currentSummary?.currentPlan,
596
+ ].join(' ')
597
+ const currentInternal = hasAnyTerm(currentText, internalProgressTerms)
598
+ const currentVisible = hasAnyTerm(currentText, visibleProgressTerms)
599
+ const triggered = recent.length >= 3 && internalRecent.length >= 3 && currentInternal && !currentVisible
600
+ return {
601
+ status: triggered ? 'warning' : 'clear',
602
+ recentRecords: recent.length,
603
+ internalRecords: internalRecent.length,
604
+ trigger: triggered ? 'repeated-internal-component-slices' : 'not-detected',
605
+ recommendation: triggered
606
+ ? 'Open a product-visible recovery slice before another internal evidence/provenance slice.'
607
+ : 'Keep checking product-visible progress against the project north star.',
608
+ limits: [
609
+ 'This is a soft steering guard, not a hard failure.',
610
+ 'It detects repeated internal/component-level evidence patterns; project owners can still choose a risk-reduction slice deliberately.',
611
+ ],
612
+ }
613
+ }
614
+
615
+ function classifyProjectType(target, state, canonicalPlan, profileText, goalMapText) {
616
+ const packageJson = readJson(path.join(target, 'package.json'))
617
+ const packageName = String(packageJson.data?.name || '').toLowerCase()
618
+ const projectName = String(state?.projectName || '').toLowerCase()
619
+ const canonicalPlanText = canonicalPlan ? readText(path.join(target, canonicalPlan)) : ''
620
+ const text = [
621
+ projectName,
622
+ packageName,
623
+ canonicalPlan,
624
+ canonicalPlanText.slice(0, 4000),
625
+ profileText,
626
+ goalMapText,
627
+ ].join(' ').toLowerCase()
628
+
629
+ if (
630
+ projectName === 'gse' ||
631
+ packageName.includes('/gse') ||
632
+ packageName === 'gse' ||
633
+ String(canonicalPlan || '').includes('.gse/gse-design-master-plan.md')
634
+ ) {
635
+ return 'skill'
636
+ }
637
+ if (hasAnyTerm(text, productProjectTerms) && !hasAnyTerm(text, skillProjectTerms)) return 'product'
638
+ if (hasAnyTerm(text, ['library', 'package', 'sdk', 'cli'])) return 'library'
639
+ if (hasAnyTerm(text, productProjectTerms)) return 'product'
640
+ return 'unknown'
641
+ }
642
+
643
+ function classifySliceType(text) {
644
+ const value = String(text || '')
645
+ const visible = hasAnyTerm(value, visibleProgressTerms)
646
+ const support = hasAnyTerm(value, internalProgressTerms) || hasAnyTerm(value, [
647
+ 'guard',
648
+ 'audit',
649
+ 'index',
650
+ 'repair',
651
+ 'validator',
652
+ 'validation',
653
+ 'preflight',
654
+ 'sync',
655
+ 'maintenance',
656
+ ])
657
+ if (visible) return 'product-visible'
658
+ if (support) return 'support'
659
+ return 'unknown'
660
+ }
661
+
662
+ function analyzeProductOutcomeGate(target, records, state, canonicalPlan, profileText, goalMapText) {
663
+ const projectType = classifyProjectType(target, state, canonicalPlan, profileText, goalMapText)
664
+ const recent = Array.isArray(records) ? records.slice(-6) : []
665
+ const currentText = [
666
+ state?.currentSlice?.id,
667
+ state?.currentSlice?.outcome,
668
+ state?.currentSlice?.nextAction,
669
+ state?.currentSummary?.currentPlan,
670
+ goalMapText,
671
+ ].join(' ')
672
+ const sliceType = classifySliceType(currentText)
673
+ const currentUserVisibleDelta = cleanInlineValue(
674
+ state?.currentSlice?.userVisibleDelta ||
675
+ state?.currentSummary?.userVisibleDelta ||
676
+ '',
677
+ )
678
+ const supportSliceBoundary = cleanInlineValue(state?.currentSlice?.supportSliceBoundary || '')
679
+ const recentSupportRecords = recent.filter((record) => {
680
+ const text = [
681
+ record?.summary,
682
+ record?.nextAction,
683
+ record?.userVisibleDelta,
684
+ Array.isArray(record?.commands) ? record.commands.join(' ') : '',
685
+ ].join(' ')
686
+ return classifySliceType(text) === 'support'
687
+ })
688
+ const recentVisibleRecords = recent.filter((record) => {
689
+ const text = [
690
+ record?.summary,
691
+ record?.nextAction,
692
+ record?.userVisibleDelta,
693
+ Array.isArray(record?.commands) ? record.commands.join(' ') : '',
694
+ ].join(' ')
695
+ return classifySliceType(text) === 'product-visible'
696
+ })
697
+ const latestVisibleDelta = currentUserVisibleDelta || cleanInlineValue([...recent].reverse().find((record) => record?.userVisibleDelta)?.userVisibleDelta || '')
698
+ const applicable = projectType === 'product'
699
+ const supportSliceStreak = recentSupportRecords.length
700
+ const warning = applicable && sliceType === 'support' && supportSliceStreak >= 3 && !latestVisibleDelta && !supportSliceBoundary
701
+ return {
702
+ status: !applicable ? 'not-applicable' : warning ? 'warning' : 'passed',
703
+ projectType,
704
+ sliceType,
705
+ userVisibleDelta: latestVisibleDelta || null,
706
+ supportSliceBoundary: supportSliceBoundary || null,
707
+ supportSliceStreak,
708
+ recentVisibleRecords: recentVisibleRecords.length,
709
+ recommendation: warning
710
+ ? 'For product work, open the next slice around a user-visible behavior, workflow, API/provider result, export, screenshot, or failure state.'
711
+ : applicable && sliceType === 'support' && supportSliceBoundary
712
+ ? `Keep this support slice within its declared boundary: ${supportSliceBoundary}`
713
+ : applicable
714
+ ? 'Keep naming the user-visible delta when product work is the mainline.'
715
+ : 'Product outcome gate is not applied to this project type.',
716
+ limits: [
717
+ 'This is a soft steering gate, not a hard blocker.',
718
+ 'Support slices remain valid when they are deliberate and bounded.',
719
+ 'supportSliceBoundary is optional metadata for the current support slice; it must name a narrow scope or exit criterion.',
720
+ 'API or workflow smoke can satisfy product-visible progress when browser evidence is not the right proof.',
721
+ ],
722
+ }
723
+ }
724
+
513
725
  function isMetaNextSliceCandidate(text) {
514
726
  const value = String(text || '').toLowerCase()
515
727
  return (
@@ -555,7 +767,7 @@ function buildCandidateActionPacket(candidate) {
555
767
  return base
556
768
  }
557
769
 
558
- function buildNextSliceCandidates(target, state, nextSliceMode) {
770
+ function buildNextSliceCandidates(target, state, nextSliceMode, productProgressDrift = null, productOutcomeGate = null) {
559
771
  if (nextSliceMode.action !== 'open-next-slice') return []
560
772
 
561
773
  const roadmapText = readText(path.join(target, 'references', 'final-form-roadmap.md'))
@@ -571,6 +783,46 @@ function buildNextSliceCandidates(target, state, nextSliceMode) {
571
783
  const candidates = []
572
784
  const firstReason = goalMapNext ? fullCandidateReason(goalMapNext) : currentNextAction
573
785
 
786
+ if (productOutcomeGate?.status === 'warning') {
787
+ candidates.push({
788
+ kind: 'product-visible-recovery',
789
+ title: 'Recover product outcome',
790
+ source: 'compactState.productOutcomeGate',
791
+ fullReason: productOutcomeGate.recommendation,
792
+ outcomeHint: 'Make the next product slice answer what the user can now do, see, run, export, or diagnose.',
793
+ scopeHint: 'Pick one visible UI, workflow, API/provider, export/import, or result path and keep internal evidence work as support only.',
794
+ acceptanceHint: 'Acceptance must name the user-visible behavior, result, failure state, or workflow step that changed.',
795
+ evidenceHint: 'Use the lightest proof that exercises the product path, such as API smoke, workflow smoke, browser smoke, screenshot, or exported result.',
796
+ riskHint: 'Do not let support work such as provenance, state, evidence, handoff, or audits replace the product mainline repeatedly.',
797
+ nextActionHint: 'Rewrite the current slice so its outcome is product-visible before changing implementation files.',
798
+ focusedChecks: [
799
+ 'node scripts\\run-gse-command.mjs --target . --command "/gse continue" --json --compact',
800
+ 'Run one focused product-visible smoke or workflow/API test for the selected path.',
801
+ ],
802
+ suggestedProfile: 'lite',
803
+ })
804
+ }
805
+
806
+ if (productProgressDrift?.status === 'warning') {
807
+ candidates.push({
808
+ kind: 'product-visible-recovery',
809
+ title: 'Recover product-visible progress',
810
+ source: 'compactState.productProgressDrift',
811
+ fullReason: productProgressDrift.recommendation,
812
+ outcomeHint: 'Make the next slice prove one user-visible workflow step or production capability instead of another internal state handoff.',
813
+ scopeHint: 'Choose a narrow UI/API/browser/provider/export path from the project north star and keep GSE as a light guardrail.',
814
+ acceptanceHint: 'Acceptance must name the user-visible behavior, result, failure state, or workflow step that changed.',
815
+ evidenceHint: 'Use the lightest focused proof that exercises the visible path, such as browser smoke, API smoke, screenshot, or product workflow test.',
816
+ riskHint: 'Do not let provenance, evidence index, state repair, or close-gate work become the product slice itself unless the owner explicitly asks for process work.',
817
+ nextActionHint: 'Rewrite current-slice around product-visible outcome before editing code.',
818
+ focusedChecks: [
819
+ 'node scripts\\run-gse-command.mjs --target . --command "/gse continue" --json --compact',
820
+ 'Run one focused product-visible smoke or workflow/API test for the selected path.',
821
+ ],
822
+ suggestedProfile: 'lite',
823
+ })
824
+ }
825
+
574
826
  const roadmapCandidate = roadmapBullets.find((item) =>
575
827
  item.toLowerCase().includes('short entry') ||
576
828
  item.toLowerCase().includes('continue') ||
@@ -762,6 +1014,8 @@ function buildContinuePacket(target) {
762
1014
  const sessionSyncBoundary = readSessionSyncBoundary(resolvedTarget)
763
1015
  const evidenceLevelAnalysis = analyzeEvidenceLevels(evidenceIndex.records)
764
1016
  const evidenceReviewQueue = analyzeEvidenceReviewQueue(evidenceIndex.records, resolvedTarget)
1017
+ const productProgressDrift = analyzeProductProgressDrift(evidenceIndex.records, state)
1018
+ const productOutcomeGate = analyzeProductOutcomeGate(resolvedTarget, evidenceIndex.records, state, canonicalPlan, profileText, goalMapText)
765
1019
  const evidenceLevelIssueCount =
766
1020
  evidenceLevelAnalysis.invalidLevel.length +
767
1021
  evidenceLevelAnalysis.downgraded.length +
@@ -901,6 +1155,22 @@ function buildContinuePacket(target) {
901
1155
  ? 'Do not infer target-session adoption from installed-copy sync or notification records.'
902
1156
  : 'Repair .gse/session-sync.jsonl before relying on sync records.',
903
1157
  ),
1158
+ makeCheck(
1159
+ 'CP22',
1160
+ 'product-visible progress drift is surfaced',
1161
+ productProgressDrift.status !== 'warning',
1162
+ `${productProgressDrift.trigger}; ${productProgressDrift.internalRecords}/${productProgressDrift.recentRecords} recent internal record(s)`,
1163
+ 'soft',
1164
+ productProgressDrift.recommendation,
1165
+ ),
1166
+ makeCheck(
1167
+ 'CP23',
1168
+ 'product outcome gate is surfaced',
1169
+ productOutcomeGate.status !== 'warning',
1170
+ `${productOutcomeGate.projectType}/${productOutcomeGate.sliceType}; supportStreak=${productOutcomeGate.supportSliceStreak}; userVisibleDelta=${productOutcomeGate.userVisibleDelta ? 'present' : 'missing'}`,
1171
+ 'soft',
1172
+ productOutcomeGate.recommendation,
1173
+ ),
904
1174
  ]
905
1175
 
906
1176
  const projectName = cleanInlineValue(
@@ -950,7 +1220,7 @@ function buildContinuePacket(target) {
950
1220
  const completionPlan = buildCompletionPlan(resolvedTarget, state, maintenanceSnapshot)
951
1221
  const gateTaxonomy = buildGateTaxonomy(blockedGates)
952
1222
  const nextSliceMode = buildNextSliceMode(state)
953
- const nextSliceCandidates = buildNextSliceCandidates(resolvedTarget, state, nextSliceMode)
1223
+ const nextSliceCandidates = buildNextSliceCandidates(resolvedTarget, state, nextSliceMode, productProgressDrift, productOutcomeGate)
954
1224
  const nextChecks = [
955
1225
  'node scripts/run-gse-command.mjs --target <project-root> --command "/gse doctor" --json',
956
1226
  ...completionPlan.requiredCloseCommands,
@@ -979,6 +1249,8 @@ function buildContinuePacket(target) {
979
1249
  nextSliceMode,
980
1250
  nextSliceCandidates,
981
1251
  nextChecks,
1252
+ productProgressDrift,
1253
+ productOutcomeGate,
982
1254
  completionPlan,
983
1255
  toolStatuses: state?.toolStatuses ?? {},
984
1256
  latestEvidence,
@@ -1158,6 +1430,7 @@ function renderMarkdown(report) {
1158
1430
  lines.push('- Host capabilities: ' + report.compactState.hostCapabilities.status + ' (' + report.compactState.hostCapabilities.total + ' row(s))')
1159
1431
  lines.push('- Maintenance snapshot: ' + report.compactState.maintenanceSnapshot.status + ' (' + report.compactState.maintenanceSnapshot.installedSyncMode + ')')
1160
1432
  lines.push('- Session sync boundary: ' + report.compactState.sessionSyncBoundary.boundary + ' (adoptionProven=' + report.compactState.sessionSyncBoundary.adoptionProven + ')')
1433
+ lines.push('- Product outcome gate: ' + report.compactState.productOutcomeGate.status + ' (' + report.compactState.productOutcomeGate.projectType + '/' + report.compactState.productOutcomeGate.sliceType + ')')
1161
1434
  lines.push('- Completion plan: ' + report.compactState.completionPlan.requiredCloseCommands.length + ' required command(s), ' + report.compactState.completionPlan.activeCloseCommands.length + ' active conditional command(s)')
1162
1435
  if (report.compactState.completionPlan.ignoredGeneratedPathCount > 0) {
1163
1436
  lines.push('- Ignored generated/noisy paths: ' + report.compactState.completionPlan.ignoredGeneratedPathCount)