@juangcarmona/lighthouse-sdlc 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -4,6 +4,8 @@ This package provides the provider-neutral delivery contract for Lighthouse adop
4
4
 
5
5
  It owns the six lifecycle skills, their command entrypoints, route, stage and gate descriptors, deterministic triage and decomposition projections, rebuildable state checks and the provider-neutral control-plane contract.
6
6
 
7
+ It also provides the `browser-e2e` evidence contract for Playwright journeys. The contract belongs to the SDLC because it records delivery evidence; the tests, journeys and browser configuration remain owned by the adopter.
8
+
7
9
  It does not run agents, store transcripts, dispatch work between repositories or replace ProductShape, ArcShape, OpenSpec or an agent host.
8
10
 
9
11
  Install it with the Lighthouse CLI or run `lighthouse-sdlc verify` from the package installation to check the distributed contract.
@@ -12,6 +14,8 @@ Run `lighthouse-sdlc triage --input <file>` to validate one structured triage re
12
14
 
13
15
  Use `lighthouse-sdlc route --route <name>` to inspect a route before running it, `stage-result --input <file>` to validate a stage result, `state --input <events.json>` to replay workflow events, `control-plane --input <file>` to validate host declarations, `decompose --input <file>` to produce a dependency-checked slice plan and `stacked --input <file>` to produce an ordered pull-request plan.
14
16
 
17
+ Use `lighthouse-sdlc browser-e2e --input <evidence.json>` to validate a Playwright journey result. It is read-only and does not start a browser or write provider state.
18
+
15
19
  Use `lighthouse-sdlc github-triage --repo <owner/repo> --issue <number> --assessment <file>` for the first GitHub projection. It reads issue metadata by default; `--apply` is the explicit label mutation boundary and rereads the issue before writing.
16
20
 
17
21
  Use `lighthouse-sdlc github-decompose --repo <owner/repo> --issue <number> --input <file>` to turn a validated slice plan into child-issue operations. The default is a preview; `--apply` creates the child issues in dependency order with the parent link and acceptance evidence in each body.
@@ -0,0 +1,10 @@
1
+ # Browser E2E
2
+
3
+ Use this command during `integrate` and `review` when the selected application exposes a browser journey.
4
+
5
+ 1. Read the ProductShape journey and its bounded context before choosing the test.
6
+ 2. Run the adopter's Playwright command and keep the report, screenshots, traces or videos under the repository's evidence path.
7
+ 3. Create a browser evidence JSON document with `runner: playwright`, the journey identity, evidence statuses, relative artifact paths and the next action.
8
+ 4. Run `lighthouse-sdlc browser-e2e --input <evidence.json>` and stop when the report is invalid or the journey is not passed.
9
+
10
+ The command validates evidence; it does not start Playwright, invent product journeys or make browser tests mandatory for applications without a browser surface.
package/contract.json CHANGED
@@ -10,7 +10,7 @@
10
10
  { "name": "integrate", "skill": "skills/integrate/SKILL.md", "command": "commands/integrate.md", "gate": "done" },
11
11
  { "name": "review", "skill": "skills/review/SKILL.md", "command": "commands/review.md", "gate": null }
12
12
  ],
13
- "utilities": ["status", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "github-triage", "github-decompose", "reconcile-labels", "github-labels"],
13
+ "utilities": ["status", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "browser-e2e", "github-triage", "github-decompose", "reconcile-labels", "github-labels"],
14
14
  "routes": ["supervised", "plan-only", "unattended", "hotfix", "stacked"],
15
15
  "metaCommands": ["ff", "hotfix", "decompose", "stacked"],
16
16
  "gates": ["triaged", "ready", "planned", "done"],
@@ -24,6 +24,7 @@
24
24
  "stateSchema": "schemas/workflow-state.schema.json",
25
25
  "decompositionSchema": "schemas/decomposition.schema.json",
26
26
  "capabilitiesSchema": "schemas/capabilities.schema.json",
27
+ "browserE2eSchema": "schemas/browser-e2e.schema.json",
27
28
  "capabilityCatalog": "capabilities.json",
28
29
  "capabilityCatalogSchema": "schemas/capability-catalog.schema.json"
29
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse-sdlc",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
6
  "repository": {
@@ -48,6 +48,7 @@
48
48
  "./capabilities": "./src/capabilities.mjs",
49
49
  "./stacked": "./src/stacked.mjs",
50
50
  "./workflow": "./src/workflow.mjs",
51
+ "./browser-e2e": "./src/browser-e2e.mjs",
51
52
  "./contract.json": "./contract.json",
52
53
  "./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
53
54
  "./schemas/triage.schema.json": "./schemas/triage.schema.json",
@@ -57,6 +58,7 @@
57
58
  "./schemas/decomposition.schema.json": "./schemas/decomposition.schema.json",
58
59
  "./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json",
59
60
  "./schemas/capability-catalog.schema.json": "./schemas/capability-catalog.schema.json",
61
+ "./schemas/browser-e2e.schema.json": "./schemas/browser-e2e.schema.json",
60
62
  "./capabilities.json": "./capabilities.json"
61
63
  }
62
64
  }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/browser-e2e.schema.json",
4
+ "title": "Lighthouse browser end-to-end evidence",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "runner", "journey", "command", "status", "evidence", "artifacts", "nextAction"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "runner": { "const": "playwright" },
11
+ "journey": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "required": ["id", "boundedContext"],
15
+ "properties": {
16
+ "id": { "type": "string", "minLength": 1 },
17
+ "boundedContext": { "type": "string", "minLength": 1 },
18
+ "title": { "type": "string", "minLength": 1 }
19
+ }
20
+ },
21
+ "command": { "type": "string", "minLength": 1 },
22
+ "status": { "enum": ["passed", "failed", "unavailable", "pending-review"] },
23
+ "evidence": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["id", "status", "source"],
29
+ "properties": {
30
+ "id": { "type": "string", "minLength": 1 },
31
+ "status": { "enum": ["passed", "failed", "unavailable", "pending-review"] },
32
+ "source": { "type": "string", "minLength": 1 },
33
+ "reason": { "type": "string", "minLength": 1 }
34
+ }
35
+ }
36
+ },
37
+ "artifacts": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["type", "path"],
43
+ "properties": {
44
+ "type": { "enum": ["screenshot", "trace", "video", "report"] },
45
+ "path": { "type": "string", "minLength": 1 }
46
+ }
47
+ }
48
+ },
49
+ "nextAction": { "type": "string", "minLength": 1 }
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: lighthouse-browser-e2e
3
+ description: Validate Playwright browser evidence for a selected product journey during integration or review.
4
+ ---
5
+
6
+ # Browser E2E
7
+
8
+ Use this skill only when the selected application has a browser surface and the product definition names a journey to verify.
9
+
10
+ Read the journey, bounded context and acceptance evidence before writing or changing a test. Keep the test in the adopter repository and use the project's existing Playwright configuration when one exists.
11
+
12
+ Run the narrowest useful journey first. Collect the report and any screenshot, trace or video needed to inspect a failure. Store relative paths in the browser evidence document and validate it with `lighthouse-sdlc browser-e2e --input <evidence.json>`.
13
+
14
+ Finish with one of `passed`, `failed`, `unavailable` or `pending-review`, plus a concrete `nextAction`. A missing artifact, unavailable browser or unresolved human judgement stays visible in the result.
@@ -0,0 +1,48 @@
1
+ const statuses = new Set(['passed', 'failed', 'unavailable', 'pending-review']);
2
+ const artifactTypes = new Set(['screenshot', 'trace', 'video', 'report']);
3
+
4
+ function issue(code, message, field) {
5
+ return field ? { code, message, field } : { code, message };
6
+ }
7
+
8
+ function record(value) {
9
+ return value && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ export function validateBrowserEvidence(value) {
13
+ const errors = [];
14
+ if (!record(value)) return [issue('browser-e2e.invalid', 'browser evidence must be an object')];
15
+ if (value.schemaVersion !== 1) errors.push(issue('browser-e2e.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
16
+ if (value.runner !== 'playwright') errors.push(issue('browser-e2e.runner', 'runner must be playwright', 'runner'));
17
+ if (!record(value.journey) || typeof value.journey.id !== 'string' || !value.journey.id.trim() || typeof value.journey.boundedContext !== 'string' || !value.journey.boundedContext.trim()) {
18
+ errors.push(issue('browser-e2e.journey', 'journey needs an id and boundedContext', 'journey'));
19
+ }
20
+ if (typeof value.command !== 'string' || !value.command.trim()) errors.push(issue('browser-e2e.command', 'command must be a non-empty string', 'command'));
21
+ if (!statuses.has(value.status)) errors.push(issue('browser-e2e.status', `status must be one of ${[...statuses].join(', ')}`, 'status'));
22
+ if (!Array.isArray(value.evidence) || value.evidence.length === 0) errors.push(issue('browser-e2e.evidence', 'evidence must contain at least one item', 'evidence'));
23
+ else {
24
+ const ids = new Set();
25
+ for (const [index, item] of value.evidence.entries()) {
26
+ if (!record(item) || typeof item.id !== 'string' || !item.id.trim() || !statuses.has(item.status) || typeof item.source !== 'string' || !item.source.trim()) {
27
+ errors.push(issue('browser-e2e.evidence-item', `evidence[${index}] needs an id, supported status and source`, `evidence[${index}]`));
28
+ continue;
29
+ }
30
+ if (ids.has(item.id)) errors.push(issue('browser-e2e.evidence-duplicate', `evidence id ${item.id} is duplicated`, `evidence[${index}]`));
31
+ ids.add(item.id);
32
+ if (['failed', 'unavailable', 'pending-review'].includes(item.status) && (typeof item.reason !== 'string' || !item.reason.trim())) errors.push(issue('browser-e2e.evidence-reason', `evidence[${index}] needs a reason for status ${item.status}`, `evidence[${index}]`));
33
+ }
34
+ }
35
+ if (!Array.isArray(value.artifacts)) errors.push(issue('browser-e2e.artifacts', 'artifacts must be an array', 'artifacts'));
36
+ else for (const [index, artifact] of value.artifacts.entries()) {
37
+ if (!record(artifact) || !artifactTypes.has(artifact.type) || typeof artifact.path !== 'string' || !artifact.path.trim() || artifact.path.startsWith('/')) errors.push(issue('browser-e2e.artifact', `artifacts[${index}] needs a relative path and supported type`, `artifacts[${index}]`));
38
+ }
39
+ if (value.status === 'passed' && (!value.artifacts?.length || value.evidence?.some((item) => item.status !== 'passed'))) errors.push(issue('browser-e2e.passed-evidence', 'passed browser evidence needs artifacts and only passed evidence items', 'status'));
40
+ if (value.status !== 'passed' && value.evidence?.every((item) => item.status === 'passed')) errors.push(issue('browser-e2e.status-evidence', `status ${value.status} needs a matching evidence item`, 'status'));
41
+ if (typeof value.nextAction !== 'string' || !value.nextAction.trim()) errors.push(issue('browser-e2e.next-action', 'nextAction must be a non-empty string', 'nextAction'));
42
+ return errors;
43
+ }
44
+
45
+ export function browserEvidenceReport(value) {
46
+ const errors = validateBrowserEvidence(value);
47
+ return { schemaVersion: 1, status: errors.length ? 'invalid' : value.status, valid: errors.length === 0, errors, journey: value.journey ?? null, runner: value.runner ?? null, artifacts: value.artifacts ?? [] };
48
+ }
package/src/sdlc.mjs CHANGED
@@ -13,6 +13,7 @@ import { applyTriageLabels, buildTriageReport } from './triage.mjs';
13
13
  import { validateMemoryRecord } from './memory.mjs';
14
14
  import { replayWorkflow } from './workflow.mjs';
15
15
  import { validateStageResult } from './contracts.mjs';
16
+ import { browserEvidenceReport } from './browser-e2e.mjs';
16
17
  import { readCapabilityCatalog, validateCapabilityCatalog } from './capabilities.mjs';
17
18
 
18
19
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -39,10 +40,10 @@ async function verifyContract() {
39
40
  for (const stage of stages) {
40
41
  for (const asset of [stage.skill, stage.command]) if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing stage asset ${asset}`);
41
42
  }
42
- for (const asset of [contract.assets?.routes, contract.assets?.gates, contract.assets?.stages, contract.assets?.controlPlaneSchema, contract.assets?.triageSchema, contract.assets?.stageResultSchema, contract.assets?.stateSchema, contract.assets?.decompositionSchema, contract.assets?.capabilitiesSchema, contract.assets?.capabilityCatalog, contract.assets?.capabilityCatalogSchema]) {
43
+ for (const asset of [contract.assets?.routes, contract.assets?.gates, contract.assets?.stages, contract.assets?.controlPlaneSchema, contract.assets?.triageSchema, contract.assets?.stageResultSchema, contract.assets?.stateSchema, contract.assets?.decompositionSchema, contract.assets?.capabilitiesSchema, contract.assets?.capabilityCatalog, contract.assets?.capabilityCatalogSchema, contract.assets?.browserE2eSchema]) {
43
44
  if (!asset || !(await exists(resolve(packageRoot, asset)))) problems.push(`missing contract asset ${asset ?? '(empty)'}`);
44
45
  }
45
- for (const asset of ['routes.json', 'gates.json', 'stages.json', 'schemas/routes.schema.json', 'schemas/gates.schema.json', 'schemas/stages.schema.json', 'schemas/control-plane.schema.json', 'schemas/triage.schema.json', 'schemas/stage-result.schema.json', 'schemas/workflow-state.schema.json', 'schemas/decomposition.schema.json', 'schemas/capabilities.schema.json', 'schemas/capability-catalog.schema.json', 'src/capabilities.mjs', 'src/contracts.mjs', 'src/control-plane.mjs', 'src/decompose.mjs', 'src/github.mjs', 'src/memory.mjs', 'src/reconcile.mjs', 'src/routes.mjs', 'src/stacked.mjs', 'src/triage.mjs', 'src/workflow.mjs']) {
46
+ for (const asset of ['routes.json', 'gates.json', 'stages.json', 'schemas/routes.schema.json', 'schemas/gates.schema.json', 'schemas/stages.schema.json', 'schemas/control-plane.schema.json', 'schemas/triage.schema.json', 'schemas/stage-result.schema.json', 'schemas/workflow-state.schema.json', 'schemas/decomposition.schema.json', 'schemas/capabilities.schema.json', 'schemas/capability-catalog.schema.json', 'schemas/browser-e2e.schema.json', 'src/browser-e2e.mjs', 'src/capabilities.mjs', 'src/contracts.mjs', 'src/control-plane.mjs', 'src/decompose.mjs', 'src/github.mjs', 'src/memory.mjs', 'src/reconcile.mjs', 'src/routes.mjs', 'src/stacked.mjs', 'src/triage.mjs', 'src/workflow.mjs']) {
46
47
  if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing package asset ${asset}`);
47
48
  }
48
49
  try {
@@ -160,6 +161,15 @@ async function runStageResult(args) {
160
161
  if (errors.length) process.exit(1);
161
162
  }
162
163
 
164
+ async function runBrowserE2e(args) {
165
+ const inputPath = option(args, '--input');
166
+ const format = option(args, '--format') ?? 'json';
167
+ if (!inputPath) { console.error('Usage: lighthouse-sdlc browser-e2e --input <evidence.json> [--format <json|text>]'); process.exit(2); }
168
+ const report = browserEvidenceReport(await readJson(resolve(process.cwd(), inputPath)));
169
+ output(report, format);
170
+ if (!report.valid || report.status !== 'passed') process.exit(1);
171
+ }
172
+
163
173
  async function runMemory(args) {
164
174
  const inputPath = option(args, '--input');
165
175
  const policyPath = option(args, '--policy');
@@ -318,8 +328,8 @@ async function runGithubLabels(args) {
318
328
  }
319
329
 
320
330
  const command = process.argv[2] ?? 'verify';
321
- if (!['contract', 'verify', 'triage', 'state', 'stage-result', 'control-plane', 'memory', 'launch', 'decompose', 'stacked', 'route', 'github-triage', 'github-decompose', 'reconcile-labels', 'github-labels'].includes(command)) {
322
- console.error('Usage: lighthouse-sdlc <contract|verify|triage|state|stage-result|control-plane|memory|launch|decompose|stacked|route|github-triage|github-decompose|reconcile-labels|github-labels>');
331
+ if (!['contract', 'verify', 'triage', 'state', 'stage-result', 'browser-e2e', 'control-plane', 'memory', 'launch', 'decompose', 'stacked', 'route', 'github-triage', 'github-decompose', 'reconcile-labels', 'github-labels'].includes(command)) {
332
+ console.error('Usage: lighthouse-sdlc <contract|verify|triage|state|stage-result|browser-e2e|control-plane|memory|launch|decompose|stacked|route|github-triage|github-decompose|reconcile-labels|github-labels>');
323
333
  process.exit(2);
324
334
  }
325
335
  if (command === 'contract') {
@@ -332,6 +342,8 @@ if (command === 'contract') {
332
342
  await runControlPlane(process.argv.slice(3));
333
343
  } else if (command === 'stage-result') {
334
344
  await runStageResult(process.argv.slice(3));
345
+ } else if (command === 'browser-e2e') {
346
+ await runBrowserE2e(process.argv.slice(3));
335
347
  } else if (command === 'memory') {
336
348
  await runMemory(process.argv.slice(3));
337
349
  } else if (command === 'launch') {
@@ -359,4 +371,4 @@ if (command === 'contract') {
359
371
  console.log('Lighthouse SDLC package contract is valid.');
360
372
  }
361
373
 
362
- export { contractPath, runTriage, verifyContract, runState, runStageResult, runControlPlane, runMemory, runLaunch, runDecompose, runRoute, runStacked, runGithubTriage, runGithubDecompose, runReconcileLabels, runGithubLabels };
374
+ export { contractPath, runTriage, verifyContract, runState, runStageResult, runBrowserE2e, runControlPlane, runMemory, runLaunch, runDecompose, runRoute, runStacked, runGithubTriage, runGithubDecompose, runReconcileLabels, runGithubLabels };