@juangcarmona/lighthouse-sdlc 0.4.0 → 0.5.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 +8 -0
- package/capabilities.json +50 -0
- package/commands/capabilities.md +13 -0
- package/contract.json +4 -2
- package/package.json +6 -2
- package/schemas/capability-catalog.schema.json +88 -0
- package/src/capabilities.mjs +84 -0
- package/src/reconcile.mjs +10 -6
- package/src/sdlc.mjs +7 -2
package/README.md
CHANGED
|
@@ -18,6 +18,14 @@ Use `lighthouse-sdlc github-decompose --repo <owner/repo> --issue <number> --inp
|
|
|
18
18
|
|
|
19
19
|
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
20
|
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
`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.
|
|
28
|
+
|
|
21
29
|
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
30
|
|
|
23
31
|
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,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" },
|
|
@@ -23,6 +23,8 @@
|
|
|
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
|
+
"capabilityCatalog": "capabilities.json",
|
|
28
|
+
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json"
|
|
27
29
|
}
|
|
28
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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,6 +45,7 @@
|
|
|
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",
|
|
49
51
|
"./contract.json": "./contract.json",
|
|
@@ -53,6 +55,8 @@
|
|
|
53
55
|
"./schemas/stage-result.schema.json": "./schemas/stage-result.schema.json",
|
|
54
56
|
"./schemas/workflow-state.schema.json": "./schemas/workflow-state.schema.json",
|
|
55
57
|
"./schemas/decomposition.schema.json": "./schemas/decomposition.schema.json",
|
|
56
|
-
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json"
|
|
58
|
+
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json",
|
|
59
|
+
"./schemas/capability-catalog.schema.json": "./schemas/capability-catalog.schema.json",
|
|
60
|
+
"./capabilities.json": "./capabilities.json"
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -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,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,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 { readCapabilityCatalog, validateCapabilityCatalog } from './capabilities.mjs';
|
|
16
17
|
|
|
17
18
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
19
|
const contractPath = resolve(packageRoot, 'contract.json');
|
|
@@ -38,10 +39,10 @@ async function verifyContract() {
|
|
|
38
39
|
for (const stage of stages) {
|
|
39
40
|
for (const asset of [stage.skill, stage.command]) if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing stage asset ${asset}`);
|
|
40
41
|
}
|
|
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]) {
|
|
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]) {
|
|
42
43
|
if (!asset || !(await exists(resolve(packageRoot, asset)))) problems.push(`missing contract asset ${asset ?? '(empty)'}`);
|
|
43
44
|
}
|
|
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']) {
|
|
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']) {
|
|
45
46
|
if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing package asset ${asset}`);
|
|
46
47
|
}
|
|
47
48
|
try {
|
|
@@ -59,6 +60,10 @@ async function verifyContract() {
|
|
|
59
60
|
const stageIds = stages.stages?.map((stage) => stage.id) ?? [];
|
|
60
61
|
if (stageIds.join(',') !== expectedStages.join(',')) problems.push('stages.json does not match contract stages');
|
|
61
62
|
} catch (error) { problems.push(`Cannot read stages.json: ${error.message}`); }
|
|
63
|
+
try {
|
|
64
|
+
const catalog = await readCapabilityCatalog();
|
|
65
|
+
problems.push(...validateCapabilityCatalog(catalog).map((error) => `${error.code}: ${error.message}`));
|
|
66
|
+
} catch (error) { problems.push(`Cannot read capabilities.json: ${error.message}`); }
|
|
62
67
|
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
68
|
for (const metaCommand of contract.metaCommands ?? []) if (!(await exists(resolve(packageRoot, `meta-commands/${metaCommand}.md`)))) problems.push(`missing meta-command asset ${metaCommand}`);
|
|
64
69
|
return problems;
|