@juangcarmona/lighthouse-sdlc 0.4.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 +12 -0
- package/capabilities.json +50 -0
- package/commands/browser-e2e.md +10 -0
- package/commands/capabilities.md +13 -0
- package/contract.json +6 -3
- package/package.json +8 -2
- package/schemas/browser-e2e.schema.json +51 -0
- package/schemas/capability-catalog.schema.json +88 -0
- package/skills/browser-e2e/SKILL.md +14 -0
- package/src/browser-e2e.mjs +48 -0
- package/src/capabilities.mjs +84 -0
- package/src/reconcile.mjs +10 -6
- package/src/sdlc.mjs +22 -5
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,12 +14,22 @@ 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.
|
|
18
22
|
|
|
19
23
|
Declare selected adoption capabilities in a JSON file that follows `schemas/capabilities.schema.json`. `reconcile-labels` previews and optionally applies the first local label projection; `github-labels` reads the repository labels through `gh`, creates only missing labels after an explicit `--apply`, preserves existing values and records `.lighthouse/reconciliation/labels.json`.
|
|
20
24
|
|
|
25
|
+
`capabilities.json` is the versioned adoption catalog. It currently declares the GitHub labels capability, its label roles and classifications, suggested colors and descriptions, declared transition policy and allowed operations.
|
|
26
|
+
|
|
27
|
+
The catalog is descriptive until a consumer creates an explicit plan. `lighthouse upgrade` can discover it, but only `lighthouse capabilities apply --plan-file ... --authorize ...` may write provider state.
|
|
28
|
+
|
|
29
|
+
Every catalog entry must name a registered adapter. Package verification fails when an entry has no adapter, and a new adapter is complete only when its plan, apply, verification, evidence and packaged-consumer fixture are covered by CI.
|
|
30
|
+
|
|
31
|
+
`selected-by-default` capabilities appear as pending until the adopter selects or declines them. `opt-in` capabilities appear as available and do not make `upgrade` or `verify` fail until the adopter selects them.
|
|
32
|
+
|
|
21
33
|
Evidence is explicit: an absent field appears in `missingEvidence`, a check that cannot run appears in `unavailableChecks`, and a judgement awaiting review appears in `pendingReview`. Only `provided` evidence can make a report ready to apply.
|
|
22
34
|
|
|
23
35
|
The package is a source of lifecycle instructions and contracts; an adopter's host adapter decides where provider-specific projections are installed.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schemas/capability-catalog.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"package": { "name": "@juangcarmona/lighthouse-sdlc", "version": "0.5.0" },
|
|
5
|
+
"capabilities": [
|
|
6
|
+
{
|
|
7
|
+
"id": "labels",
|
|
8
|
+
"provider": "github",
|
|
9
|
+
"adapter": "github-labels",
|
|
10
|
+
"version": "1",
|
|
11
|
+
"title": "Lighthouse tracker labels",
|
|
12
|
+
"selection": "selected-by-default",
|
|
13
|
+
"configuration": {
|
|
14
|
+
"roles": ["state", "command", "classification"],
|
|
15
|
+
"labels": [
|
|
16
|
+
{ "name": "triage", "role": "state", "color": "8B949E", "description": "Work item is being assessed." },
|
|
17
|
+
{ "name": "refine", "role": "state", "color": "4DA3FF", "description": "Work item is being refined." },
|
|
18
|
+
{ "name": "implement", "role": "state", "color": "1F6FEB", "description": "Work item is being implemented." },
|
|
19
|
+
{ "name": "wip", "role": "state", "color": "D29922", "description": "Work item is actively in progress." },
|
|
20
|
+
{ "name": "review", "role": "state", "color": "A371F7", "description": "Work item is awaiting review." },
|
|
21
|
+
{ "name": "size-s", "role": "classification", "dimension": "size", "value": "S", "color": "BFE3FF", "description": "Small work item." },
|
|
22
|
+
{ "name": "size-m", "role": "classification", "dimension": "size", "value": "M", "color": "80C2FF", "description": "Medium work item." },
|
|
23
|
+
{ "name": "size-l", "role": "classification", "dimension": "size", "value": "L", "color": "4DA3FF", "description": "Large work item." },
|
|
24
|
+
{ "name": "size-xl", "role": "classification", "dimension": "size", "value": "XL", "color": "1F6FEB", "description": "Extra-large work item." },
|
|
25
|
+
{ "name": "urgency-normal", "role": "classification", "dimension": "urgency", "value": "normal", "color": "8B949E", "description": "Normal urgency." },
|
|
26
|
+
{ "name": "urgency-asap", "role": "classification", "dimension": "urgency", "value": "asap", "color": "D29922", "description": "High urgency." },
|
|
27
|
+
{ "name": "urgency-now", "role": "classification", "dimension": "urgency", "value": "now", "color": "CF222E", "description": "Immediate urgency." },
|
|
28
|
+
{ "name": "impact-low", "role": "classification", "dimension": "impact", "value": "low", "color": "E9D5FF", "description": "Low impact." },
|
|
29
|
+
{ "name": "impact-medium", "role": "classification", "dimension": "impact", "value": "medium", "color": "A371F7", "description": "Medium impact." },
|
|
30
|
+
{ "name": "impact-high", "role": "classification", "dimension": "impact", "value": "high", "color": "6E40C9", "description": "High impact." },
|
|
31
|
+
{ "name": "risk-low", "role": "classification", "dimension": "risk", "value": "low", "color": "FFEBE9", "description": "Low delivery risk." },
|
|
32
|
+
{ "name": "risk-medium", "role": "classification", "dimension": "risk", "value": "medium", "color": "F0883E", "description": "Medium delivery risk." },
|
|
33
|
+
{ "name": "risk-high", "role": "classification", "dimension": "risk", "value": "high", "color": "CF222E", "description": "High delivery risk." },
|
|
34
|
+
{ "name": "blast-radius-low", "role": "classification", "dimension": "blast-radius", "value": "low", "color": "C7F0E8", "description": "Small affected surface." },
|
|
35
|
+
{ "name": "blast-radius-medium", "role": "classification", "dimension": "blast-radius", "value": "medium", "color": "4FB3A4", "description": "Medium affected surface." },
|
|
36
|
+
{ "name": "blast-radius-high", "role": "classification", "dimension": "blast-radius", "value": "high", "color": "0F766E", "description": "Large affected surface." }
|
|
37
|
+
],
|
|
38
|
+
"transitions": [{ "from": "triage", "to": "refine" }],
|
|
39
|
+
"transitionPolicy": {
|
|
40
|
+
"mode": "declared-only",
|
|
41
|
+
"description": "Only transitions declared by the adopter are legal; the catalog supplies the initial triage-to-refine example."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"operations": {
|
|
45
|
+
"allowed": ["create", "preserve"],
|
|
46
|
+
"blocked": ["modify", "rename", "remove", "ambiguous"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -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.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Capability adoption
|
|
2
|
+
|
|
3
|
+
The SDLC package publishes `capabilities.json` as the versioned catalog of adoptable provider capabilities.
|
|
4
|
+
|
|
5
|
+
The Lighthouse CLI reads this catalog during `upgrade`; package installation and upgrade never write to an external provider.
|
|
6
|
+
|
|
7
|
+
Use `lighthouse capabilities select --id labels --provider github` to make the default selection explicit, or `lighthouse capabilities decline --id labels --provider github` to record an intentional opt-out without contacting GitHub.
|
|
8
|
+
|
|
9
|
+
Use `lighthouse capabilities plan --id labels --provider github --repo <owner/repo> --plan-file .lighthouse/reconciliation/labels.plan.json` to read the current labels and produce a deterministic plan; pass `--input <capabilities.json>` when the adopter declares a mapping instead of using the catalog projection.
|
|
10
|
+
|
|
11
|
+
Use `lighthouse capabilities apply --plan-file .lighthouse/reconciliation/labels.plan.json --authorize <plan-hash>` only after reviewing the plan; the command rereads GitHub, creates missing labels, preserves existing values and records evidence.
|
|
12
|
+
|
|
13
|
+
The plan blocks modifications, renames, removals and ambiguous matches; rerunning the same plan is safe and a failed run can resume after a fresh read.
|
package/contract.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./schemas/sdlc-contract.schema.json",
|
|
3
3
|
"name": "@juangcarmona/lighthouse-sdlc",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"stages": [
|
|
6
6
|
{ "name": "triage", "skill": "skills/triage/SKILL.md", "command": "commands/triage.md", "gate": "triaged" },
|
|
7
7
|
{ "name": "refine", "skill": "skills/refine/SKILL.md", "command": "commands/refine.md", "gate": "ready" },
|
|
@@ -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"],
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"stageResultSchema": "schemas/stage-result.schema.json",
|
|
24
24
|
"stateSchema": "schemas/workflow-state.schema.json",
|
|
25
25
|
"decompositionSchema": "schemas/decomposition.schema.json",
|
|
26
|
-
"capabilitiesSchema": "schemas/capabilities.schema.json"
|
|
26
|
+
"capabilitiesSchema": "schemas/capabilities.schema.json",
|
|
27
|
+
"browserE2eSchema": "schemas/browser-e2e.schema.json",
|
|
28
|
+
"capabilityCatalog": "capabilities.json",
|
|
29
|
+
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json"
|
|
27
30
|
}
|
|
28
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse-sdlc",
|
|
3
|
-
"version": "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": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"commands",
|
|
24
24
|
"contract.json",
|
|
25
|
+
"capabilities.json",
|
|
25
26
|
"gates.json",
|
|
26
27
|
"meta-commands",
|
|
27
28
|
"README.md",
|
|
@@ -44,8 +45,10 @@
|
|
|
44
45
|
"./memory": "./src/memory.mjs",
|
|
45
46
|
"./routes": "./src/routes.mjs",
|
|
46
47
|
"./reconcile": "./src/reconcile.mjs",
|
|
48
|
+
"./capabilities": "./src/capabilities.mjs",
|
|
47
49
|
"./stacked": "./src/stacked.mjs",
|
|
48
50
|
"./workflow": "./src/workflow.mjs",
|
|
51
|
+
"./browser-e2e": "./src/browser-e2e.mjs",
|
|
49
52
|
"./contract.json": "./contract.json",
|
|
50
53
|
"./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
|
|
51
54
|
"./schemas/triage.schema.json": "./schemas/triage.schema.json",
|
|
@@ -53,6 +56,9 @@
|
|
|
53
56
|
"./schemas/stage-result.schema.json": "./schemas/stage-result.schema.json",
|
|
54
57
|
"./schemas/workflow-state.schema.json": "./schemas/workflow-state.schema.json",
|
|
55
58
|
"./schemas/decomposition.schema.json": "./schemas/decomposition.schema.json",
|
|
56
|
-
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json"
|
|
59
|
+
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json",
|
|
60
|
+
"./schemas/capability-catalog.schema.json": "./schemas/capability-catalog.schema.json",
|
|
61
|
+
"./schemas/browser-e2e.schema.json": "./schemas/browser-e2e.schema.json",
|
|
62
|
+
"./capabilities.json": "./capabilities.json"
|
|
57
63
|
}
|
|
58
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,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/capability-catalog.schema.json",
|
|
4
|
+
"title": "Lighthouse adoptable capability catalog",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "package", "capabilities"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"package": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"required": ["name", "version"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"name": { "const": "@juangcarmona/lighthouse-sdlc" },
|
|
17
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"capabilities": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["id", "provider", "adapter", "version", "title", "selection", "configuration", "operations"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"id": { "type": "string", "minLength": 1 },
|
|
29
|
+
"provider": { "type": "string", "minLength": 1 },
|
|
30
|
+
"adapter": { "type": "string", "minLength": 1 },
|
|
31
|
+
"version": { "type": "string", "minLength": 1 },
|
|
32
|
+
"title": { "type": "string", "minLength": 1 },
|
|
33
|
+
"selection": { "enum": ["selected-by-default", "opt-in"] },
|
|
34
|
+
"configuration": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": true,
|
|
37
|
+
"properties": {
|
|
38
|
+
"roles": { "type": "array", "items": { "enum": ["state", "command", "classification"] } },
|
|
39
|
+
"labels": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["name", "role", "description"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"name": { "type": "string", "minLength": 1 },
|
|
47
|
+
"role": { "enum": ["state", "command", "classification"] },
|
|
48
|
+
"dimension": { "type": "string" },
|
|
49
|
+
"value": { "type": "string" },
|
|
50
|
+
"color": { "type": "string", "pattern": "^[0-9A-Fa-f]{6}$" },
|
|
51
|
+
"description": { "type": "string", "minLength": 1 }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"transitions": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"required": ["from", "to"],
|
|
61
|
+
"properties": { "from": { "type": "string" }, "to": { "type": "string" } }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"transitionPolicy": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["mode", "description"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"mode": { "enum": ["declared-only"] },
|
|
70
|
+
"description": { "type": "string", "minLength": 1 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"operations": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"additionalProperties": false,
|
|
78
|
+
"required": ["allowed", "blocked"],
|
|
79
|
+
"properties": {
|
|
80
|
+
"allowed": { "type": "array", "items": { "enum": ["create", "preserve", "modify", "rename", "remove", "ambiguous", "unsupported"] } },
|
|
81
|
+
"blocked": { "type": "array", "items": { "enum": ["create", "preserve", "modify", "rename", "remove", "ambiguous", "unsupported"] } }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function issue(code, message, field) {
|
|
12
|
+
return field ? { code, message, field } : { code, message };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const capabilityAdapters = Object.freeze({
|
|
16
|
+
'labels/github': 'github-labels',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function capabilityAdapter(capability) {
|
|
20
|
+
return capabilityAdapters[`${capability?.id ?? ''}/${capability?.provider ?? ''}`] ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function initialAdoptionStatus(capability) {
|
|
24
|
+
return capability?.selection === 'opt-in' ? 'available' : 'pending-adoption';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readCapabilityCatalog(path = resolve(packageRoot, 'capabilities.json')) {
|
|
28
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validateCapabilityCatalog(catalog) {
|
|
32
|
+
const errors = [];
|
|
33
|
+
if (!isRecord(catalog)) return [issue('catalog.invalid', 'capability catalog must be an object')];
|
|
34
|
+
if (catalog.schemaVersion !== 1) errors.push(issue('catalog.schema-version', 'catalog schemaVersion must be 1', 'schemaVersion'));
|
|
35
|
+
if (catalog.package?.name !== '@juangcarmona/lighthouse-sdlc') errors.push(issue('catalog.package', 'catalog package name is incorrect', 'package.name'));
|
|
36
|
+
if (typeof catalog.package?.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(catalog.package.version)) errors.push(issue('catalog.package-version', 'catalog package version must be semver-like', 'package.version'));
|
|
37
|
+
if (!Array.isArray(catalog.capabilities) || catalog.capabilities.length === 0) return [...errors, issue('catalog.capabilities', 'catalog capabilities must be a non-empty array', 'capabilities')];
|
|
38
|
+
const ids = new Set();
|
|
39
|
+
for (const capability of catalog.capabilities) {
|
|
40
|
+
if (!isRecord(capability) || typeof capability.id !== 'string' || !capability.id.trim()) {
|
|
41
|
+
errors.push(issue('catalog.capability', 'every catalog capability needs an id', 'capabilities'));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (ids.has(capability.id)) errors.push(issue('catalog.duplicate', `catalog capability ${capability.id} appears more than once`, 'capabilities'));
|
|
45
|
+
ids.add(capability.id);
|
|
46
|
+
if (typeof capability.provider !== 'string' || !capability.provider) errors.push(issue('catalog.provider', `catalog capability ${capability.id} needs a provider`, 'capabilities'));
|
|
47
|
+
if (typeof capability.version !== 'string' || !capability.version) errors.push(issue('catalog.version', `catalog capability ${capability.id} needs a version`, 'capabilities'));
|
|
48
|
+
if (typeof capability.adapter !== 'string' || !capability.adapter) errors.push(issue('catalog.adapter', `catalog capability ${capability.id} needs an adapter`, 'capabilities'));
|
|
49
|
+
else if (capability.adapter !== capabilityAdapter(capability)) errors.push(issue('catalog.adapter-unsupported', `catalog capability ${capability.id} does not have a registered ${capability.adapter} adapter`, 'capabilities'));
|
|
50
|
+
if (!isRecord(capability.configuration)) errors.push(issue('catalog.configuration', `catalog capability ${capability.id} needs a configuration object`, 'capabilities'));
|
|
51
|
+
if (capability.adapter === 'github-labels' && (!Array.isArray(capability.configuration?.labels) || !Array.isArray(capability.configuration?.transitions))) {
|
|
52
|
+
errors.push(issue('catalog.configuration', `catalog capability ${capability.id} needs labels and transitions for the github-labels adapter`, 'capabilities'));
|
|
53
|
+
}
|
|
54
|
+
if (!isRecord(capability.operations) || !Array.isArray(capability.operations.allowed) || !Array.isArray(capability.operations.blocked)) errors.push(issue('catalog.operations', `catalog capability ${capability.id} needs operation policy`, 'capabilities'));
|
|
55
|
+
else {
|
|
56
|
+
const allowed = new Set(capability.operations.allowed);
|
|
57
|
+
const blocked = new Set(capability.operations.blocked);
|
|
58
|
+
const operationKinds = new Set(['create', 'preserve', 'modify', 'rename', 'remove', 'ambiguous', 'unsupported']);
|
|
59
|
+
for (const kind of [...allowed, ...blocked]) if (!operationKinds.has(kind)) errors.push(issue('catalog.operation-kind', `catalog capability ${capability.id} uses unknown operation ${kind}`, 'capabilities'));
|
|
60
|
+
for (const kind of allowed) if (blocked.has(kind)) errors.push(issue('catalog.operation-overlap', `catalog capability ${capability.id} marks ${kind} as both allowed and blocked`, 'capabilities'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return errors;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function catalogCapability(catalog, id) {
|
|
67
|
+
return catalog?.capabilities?.find((capability) => capability.id === id) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function declarationFromCatalog(catalog, id = 'labels') {
|
|
71
|
+
const capability = catalogCapability(catalog, id);
|
|
72
|
+
if (!capability) throw new Error(`Capability ${id} is not declared by the installed SDLC package.`);
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
capabilities: [{
|
|
76
|
+
id: capability.id,
|
|
77
|
+
provider: capability.provider,
|
|
78
|
+
version: capability.version,
|
|
79
|
+
adapter: capability.adapter,
|
|
80
|
+
catalogVersion: catalog.package.version,
|
|
81
|
+
configuration: capability.configuration,
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/reconcile.mjs
CHANGED
|
@@ -111,17 +111,17 @@ export function buildLabelReconciliationPlan(declaration, existingLabels, previo
|
|
|
111
111
|
if (!current) {
|
|
112
112
|
const equivalents = existingByComparableName.get(comparableName(label.name)) ?? [];
|
|
113
113
|
if (equivalents.length) {
|
|
114
|
-
changes.push({ name: label.name, status: 'conflict', desired: label, candidates: equivalents });
|
|
114
|
+
changes.push({ name: label.name, status: 'conflict', operation: 'ambiguous', desired: label, candidates: equivalents });
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
-
changes.push({ name: label.name, status: 'missing', desired: label });
|
|
118
|
-
operations.push({ type: 'create-label', name: label.name, color: label.color ?? '8B949E', description: label.description ?? '' });
|
|
117
|
+
changes.push({ name: label.name, status: 'missing', operation: 'create', desired: label });
|
|
118
|
+
operations.push({ type: 'create-label', kind: 'create', name: label.name, color: label.color ?? '8B949E', description: label.description ?? '' });
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
121
|
const customized = (label.color !== null && current.color !== label.color) || (label.description !== null && current.description !== label.description);
|
|
122
|
-
changes.push({ name: label.name, status: customized ? 'customized' : 'unchanged', existing: current, desired: label });
|
|
122
|
+
changes.push({ name: label.name, status: customized ? 'customized' : 'unchanged', operation: 'preserve', blockedOperations: customized ? ['modify'] : [], existing: current, desired: label });
|
|
123
123
|
}
|
|
124
|
-
for (const label of existing.labels) if (!desiredNames.has(label.name)) changes.push({ name: label.name, status: 'preserved', existing: label });
|
|
124
|
+
for (const label of existing.labels) if (!desiredNames.has(label.name)) changes.push({ name: label.name, status: 'preserved', operation: 'preserve', existing: label });
|
|
125
125
|
changes.sort((left, right) => left.name.localeCompare(right.name));
|
|
126
126
|
const target = [...existing.labels, ...operations.map((operation) => ({ name: operation.name, color: operation.color, description: operation.description }))].sort((left, right) => left.name.localeCompare(right.name));
|
|
127
127
|
const observedHash = digest(existing.labels);
|
|
@@ -140,7 +140,10 @@ export function buildLabelReconciliationPlan(declaration, existingLabels, previo
|
|
|
140
140
|
desiredHash: digest(desired),
|
|
141
141
|
observedHash,
|
|
142
142
|
targetHash,
|
|
143
|
+
target,
|
|
143
144
|
drift,
|
|
145
|
+
operationPolicy: capability?.operations ?? { allowed: ['create', 'preserve'], blocked: ['modify', 'rename', 'remove', 'ambiguous'] },
|
|
146
|
+
blockedOperations: changes.flatMap((change) => change.blockedOperations ?? []),
|
|
144
147
|
changes,
|
|
145
148
|
operations,
|
|
146
149
|
errors,
|
|
@@ -150,7 +153,8 @@ export function buildLabelReconciliationPlan(declaration, existingLabels, previo
|
|
|
150
153
|
desiredHash: digest(desired),
|
|
151
154
|
observedHash: targetHash,
|
|
152
155
|
targetHash,
|
|
153
|
-
operations: operations.map(({ type, name, color: labelColor, description }) => ({ type, name, color: labelColor, description })),
|
|
156
|
+
operations: operations.map(({ type, kind, name, color: labelColor, description }) => ({ type, kind, name, color: labelColor, description })),
|
|
157
|
+
blockedOperations: changes.flatMap((change) => change.blockedOperations ?? []),
|
|
154
158
|
verified: false,
|
|
155
159
|
},
|
|
156
160
|
};
|
package/src/sdlc.mjs
CHANGED
|
@@ -13,6 +13,8 @@ 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';
|
|
17
|
+
import { readCapabilityCatalog, validateCapabilityCatalog } from './capabilities.mjs';
|
|
16
18
|
|
|
17
19
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
20
|
const contractPath = resolve(packageRoot, 'contract.json');
|
|
@@ -38,10 +40,10 @@ async function verifyContract() {
|
|
|
38
40
|
for (const stage of stages) {
|
|
39
41
|
for (const asset of [stage.skill, stage.command]) if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing stage asset ${asset}`);
|
|
40
42
|
}
|
|
41
|
-
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]) {
|
|
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]) {
|
|
42
44
|
if (!asset || !(await exists(resolve(packageRoot, asset)))) problems.push(`missing contract asset ${asset ?? '(empty)'}`);
|
|
43
45
|
}
|
|
44
|
-
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', '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']) {
|
|
45
47
|
if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing package asset ${asset}`);
|
|
46
48
|
}
|
|
47
49
|
try {
|
|
@@ -59,6 +61,10 @@ async function verifyContract() {
|
|
|
59
61
|
const stageIds = stages.stages?.map((stage) => stage.id) ?? [];
|
|
60
62
|
if (stageIds.join(',') !== expectedStages.join(',')) problems.push('stages.json does not match contract stages');
|
|
61
63
|
} catch (error) { problems.push(`Cannot read stages.json: ${error.message}`); }
|
|
64
|
+
try {
|
|
65
|
+
const catalog = await readCapabilityCatalog();
|
|
66
|
+
problems.push(...validateCapabilityCatalog(catalog).map((error) => `${error.code}: ${error.message}`));
|
|
67
|
+
} catch (error) { problems.push(`Cannot read capabilities.json: ${error.message}`); }
|
|
62
68
|
for (const command of ['status', ...expectedStages, ...(contract.utilities ?? []).filter((command) => command !== 'status')]) if (!(await exists(resolve(packageRoot, `commands/${command}.md`)))) problems.push(`missing command asset ${command}`);
|
|
63
69
|
for (const metaCommand of contract.metaCommands ?? []) if (!(await exists(resolve(packageRoot, `meta-commands/${metaCommand}.md`)))) problems.push(`missing meta-command asset ${metaCommand}`);
|
|
64
70
|
return problems;
|
|
@@ -155,6 +161,15 @@ async function runStageResult(args) {
|
|
|
155
161
|
if (errors.length) process.exit(1);
|
|
156
162
|
}
|
|
157
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
|
+
|
|
158
173
|
async function runMemory(args) {
|
|
159
174
|
const inputPath = option(args, '--input');
|
|
160
175
|
const policyPath = option(args, '--policy');
|
|
@@ -313,8 +328,8 @@ async function runGithubLabels(args) {
|
|
|
313
328
|
}
|
|
314
329
|
|
|
315
330
|
const command = process.argv[2] ?? 'verify';
|
|
316
|
-
if (!['contract', 'verify', 'triage', 'state', 'stage-result', 'control-plane', 'memory', 'launch', 'decompose', 'stacked', 'route', 'github-triage', 'github-decompose', 'reconcile-labels', 'github-labels'].includes(command)) {
|
|
317
|
-
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>');
|
|
318
333
|
process.exit(2);
|
|
319
334
|
}
|
|
320
335
|
if (command === 'contract') {
|
|
@@ -327,6 +342,8 @@ if (command === 'contract') {
|
|
|
327
342
|
await runControlPlane(process.argv.slice(3));
|
|
328
343
|
} else if (command === 'stage-result') {
|
|
329
344
|
await runStageResult(process.argv.slice(3));
|
|
345
|
+
} else if (command === 'browser-e2e') {
|
|
346
|
+
await runBrowserE2e(process.argv.slice(3));
|
|
330
347
|
} else if (command === 'memory') {
|
|
331
348
|
await runMemory(process.argv.slice(3));
|
|
332
349
|
} else if (command === 'launch') {
|
|
@@ -354,4 +371,4 @@ if (command === 'contract') {
|
|
|
354
371
|
console.log('Lighthouse SDLC package contract is valid.');
|
|
355
372
|
}
|
|
356
373
|
|
|
357
|
-
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 };
|