cyber-sdd 0.6.0 → 0.7.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.plugin/plugin.json +1 -1
- package/agents/sdd-automaton.md +2 -3
- package/agents/sdd-impl-judge.md +5 -4
- package/agents/sdd-scanner.md +1 -1
- package/agents/sdd-spec-judge.md +3 -3
- package/agents/sdd-warden.md +8 -0
- package/dist/check-project-specs/scripts/check-project-specs.js +1 -0
- package/package.json +2 -2
- package/skills/align-spec/README.md +3 -3
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.bundle.mjs +6753 -0
- package/skills/align-spec/scripts/align-spec.mjs +12 -0
- package/skills/architect-impl-governance/SKILL.md +3 -1
- package/skills/architect-spec-governance/SKILL.md +5 -1
- package/skills/blast-estimate/README.md +1 -1
- package/skills/blast-estimate/SKILL.md +1 -1
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +593 -0
- package/skills/blast-estimate/scripts/blast-estimate.mjs +12 -0
- package/skills/check-field-mandates/SKILL.md +1 -1
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +251 -0
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +12 -0
- package/skills/check-partition-quality/SKILL.md +1 -1
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +233 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +12 -0
- package/skills/check-plan-safety/README.md +3 -3
- package/skills/check-plan-safety/SKILL.md +1 -1
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +107 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +12 -0
- package/skills/check-plugin-manifests/SKILL.md +1 -1
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +248 -0
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +12 -0
- package/skills/check-project-specs/README.md +3 -3
- package/skills/check-project-specs/SKILL.md +2 -2
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +584 -0
- package/skills/check-project-specs/scripts/check-project-specs.mjs +12 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +1 -0
- package/skills/check-retired-terms/README.md +2 -2
- package/skills/check-retired-terms/SKILL.md +2 -2
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +184 -0
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +12 -0
- package/skills/check-scenario-overlap/README.md +2 -2
- package/skills/check-scenario-overlap/SKILL.md +2 -3
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +175 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +12 -0
- package/skills/check-spec-references/README.md +1 -1
- package/skills/check-spec-references/SKILL.md +1 -1
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +285 -0
- package/skills/check-spec-references/scripts/check-spec-references.mjs +12 -0
- package/skills/check-spec-structure/README.md +2 -2
- package/skills/check-spec-structure/SKILL.md +1 -1
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +241 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +12 -0
- package/skills/collision-ladder/SKILL.md +3 -3
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +7041 -0
- package/skills/collision-ladder/scripts/collision-ladder.mjs +12 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +4 -2
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/README.md +2 -2
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.bundle.mjs +181 -0
- package/skills/concept-index/scripts/concept-index.mjs +12 -0
- package/skills/discover-plans/README.md +2 -2
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +135 -0
- package/skills/discover-plans/scripts/discover-plans.mjs +12 -0
- package/skills/discover-specs/README.md +1 -1
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +319 -0
- package/skills/discover-specs/scripts/discover-specs.mjs +12 -0
- package/skills/doctrine-loop/SKILL.md +2 -2
- package/skills/formation-loop/SKILL.md +11 -4
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +3 -3
- package/skills/init/SKILL.md +3 -3
- package/skills/init/scripts/wire-statusline.bundle.mjs +166 -0
- package/skills/init/scripts/wire-statusline.mjs +12 -0
- package/skills/manage-ignore/README.md +5 -5
- package/skills/manage-ignore/SKILL.md +1 -1
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +244 -0
- package/skills/manage-ignore/scripts/manage-ignore.mjs +12 -0
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +237 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +12 -0
- package/skills/manage-spec-anchors/README.md +4 -4
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +308 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +12 -0
- package/skills/mission-graph/SKILL.md +9 -9
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +1038 -0
- package/skills/mission-graph/scripts/mission-graph.mjs +12 -0
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/README.md +1 -1
- package/skills/place-node/SKILL.md +1 -1
- package/skills/place-node/scripts/place-node.bundle.mjs +115 -0
- package/skills/place-node/scripts/place-node.mjs +12 -0
- package/skills/plan-retirement/SKILL.md +1 -1
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +121 -0
- package/skills/plan-retirement/scripts/retire-plans.mjs +12 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +6 -0
- package/skills/plugin-contract-governance/SKILL.md +5 -12
- package/skills/resolve-governances/README.md +1 -1
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +350 -0
- package/skills/resolve-governances/scripts/resolve-governances.mjs +12 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +1 -1
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +150 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +12 -0
- package/skills/scaffold-project-spec/SKILL.md +2 -2
- package/skills/sdd/SKILL.md +1 -1
- package/skills/solution-producer-governance/README.md +1 -1
- package/skills/solution-producer-governance/SKILL.md +1 -1
- package/skills/spec-gate/SKILL.md +5 -5
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +420 -0
- package/skills/spec-gate/scripts/check-spec-state.mjs +12 -0
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +6498 -0
- package/skills/spec-gate/scripts/check-suite.mjs +12 -0
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +6577 -0
- package/skills/spec-gate/scripts/classify-edit-class.mjs +12 -0
- package/skills/spec-producer-governance/README.md +1 -1
- package/skills/spec-producer-governance/SKILL.md +3 -3
- package/skills/spec-structure-governance/README.md +4 -2
- package/skills/spec-structure-governance/SKILL.md +42 -7
- package/skills/start-mission/README.md +1 -1
- package/skills/start-mission/SKILL.md +6 -6
- package/skills/suite-format-governance/references/rubric.md +1 -1
- package/skills/touch-set-correction/SKILL.md +2 -2
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +6655 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +12 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +12 -3
- package/skills/verify-scenarios/README.md +3 -3
- package/skills/verify-scenarios/SKILL.md +3 -3
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +6537 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +12 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
|
|
2
|
+
// A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
|
|
3
|
+
// will not strip types, so it runs the self-contained bundle built for the package.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const self = fileURLToPath(import.meta.url)
|
|
8
|
+
const installed = /[\\/]node_modules[\\/]/.test(self)
|
|
9
|
+
const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
|
|
10
|
+
const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
11
|
+
if (run.error) throw run.error
|
|
12
|
+
process.exitCode = run.status ?? 1
|
|
@@ -26,7 +26,9 @@ unbound.
|
|
|
26
26
|
- **No conflict.** The implementation does not contradict conventions, module boundaries, or a spec's
|
|
27
27
|
contract — including the project's **declared** layout, judged as declared
|
|
28
28
|
(`sdd:spec-structure-governance`). The declaration is not a licence: node <-> capability stays 1:1
|
|
29
|
-
under every strategy, because the scheduler cuts one mission per node (ADR-0025).
|
|
29
|
+
under every strategy, because the scheduler cuts one mission per node (ADR-0025). The two-level
|
|
30
|
+
depth cap covers capability nodes; `design/` keeps its own shape — perspective files at the top and
|
|
31
|
+
at most one version folder the placement map names for the next design.
|
|
30
32
|
- **Contained complexity.** Watch cyclomatic complexity and coupling; a unit hard to test is usually
|
|
31
33
|
mis-structured — flag it.
|
|
32
34
|
- **A sound verification pyramid.** Across the suite the verification levels form a pyramid — a broad
|
|
@@ -34,7 +34,11 @@ unbound.
|
|
|
34
34
|
they keep **node <-> capability 1:1**, because the mission scheduler cuts one mission per node and a
|
|
35
35
|
scattered capability degrades the schedule toward serial (ADR-0025). **One capability per node,
|
|
36
36
|
never smeared across nodes**, holds under every strategy, and a layered / framework-first *top*
|
|
37
|
-
level stays discouraged however it is declared.
|
|
37
|
+
level stays discouraged however it is declared. Depth is judged by kind: a **capability node** sits
|
|
38
|
+
at most `<capability>/<unit>`, a sub-grouping going to a `concept:` tag; a **design doc** sits at
|
|
39
|
+
the top of `design/` as one file per perspective, or in the version folder the placement map names
|
|
40
|
+
for the next design — never in a perspective folder, and never in a sibling top-level folder such
|
|
41
|
+
as `design-next/`.
|
|
38
42
|
- **A well-formed CFG.** Its control-flow graph connects — every decision reachable, no
|
|
39
43
|
dangling branch — and the suite's sections mirror it.
|
|
40
44
|
- **The CFG reaches every stated extension.** A use case's **extensions** are its divergence paths
|
|
@@ -20,7 +20,7 @@ across **both**, so centrality counts implementation dependency rather than spec
|
|
|
20
20
|
Layouts are injected, so tests construct them as fixtures.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
node scripts/blast-estimate.
|
|
23
|
+
node scripts/blast-estimate.mjs --root <corpus> --touch-set sdd/mission-graph,sdd/blast-estimate --declared medium
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Read-only, deterministic, reports-never-writes — the mission-graph's single writer records the
|
|
@@ -107,7 +107,7 @@ are scored, so a one-area touch-set costs one matcher pass over the corpus rathe
|
|
|
107
107
|
## Run it
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
node "<skill>/scripts/blast-estimate.
|
|
110
|
+
node "<skill>/scripts/blast-estimate.mjs" --root <repo> --touch-set a,b,c \
|
|
111
111
|
[--layout 'sdd:.agents/specs/sdd,plugins/sdd/skills']... \
|
|
112
112
|
[--declared low|medium|high|unknown] [--format toon|json]
|
|
113
113
|
```
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
//#region skills/spec-gate/vendor/gherkin-cli.mjs
|
|
7
|
+
var TokenType;
|
|
8
|
+
(function(TokenType) {
|
|
9
|
+
TokenType[TokenType["None"] = 0] = "None";
|
|
10
|
+
TokenType[TokenType["EOF"] = 1] = "EOF";
|
|
11
|
+
TokenType[TokenType["Empty"] = 2] = "Empty";
|
|
12
|
+
TokenType[TokenType["Comment"] = 3] = "Comment";
|
|
13
|
+
TokenType[TokenType["TagLine"] = 4] = "TagLine";
|
|
14
|
+
TokenType[TokenType["FeatureLine"] = 5] = "FeatureLine";
|
|
15
|
+
TokenType[TokenType["RuleLine"] = 6] = "RuleLine";
|
|
16
|
+
TokenType[TokenType["BackgroundLine"] = 7] = "BackgroundLine";
|
|
17
|
+
TokenType[TokenType["ScenarioLine"] = 8] = "ScenarioLine";
|
|
18
|
+
TokenType[TokenType["ExamplesLine"] = 9] = "ExamplesLine";
|
|
19
|
+
TokenType[TokenType["StepLine"] = 10] = "StepLine";
|
|
20
|
+
TokenType[TokenType["DocStringSeparator"] = 11] = "DocStringSeparator";
|
|
21
|
+
TokenType[TokenType["TableRow"] = 12] = "TableRow";
|
|
22
|
+
TokenType[TokenType["Language"] = 13] = "Language";
|
|
23
|
+
TokenType[TokenType["Other"] = 14] = "Other";
|
|
24
|
+
})(TokenType || (TokenType = {}));
|
|
25
|
+
var RuleType;
|
|
26
|
+
(function(RuleType) {
|
|
27
|
+
RuleType[RuleType["None"] = 0] = "None";
|
|
28
|
+
RuleType[RuleType["_EOF"] = 1] = "_EOF";
|
|
29
|
+
RuleType[RuleType["_Empty"] = 2] = "_Empty";
|
|
30
|
+
RuleType[RuleType["_Comment"] = 3] = "_Comment";
|
|
31
|
+
RuleType[RuleType["_TagLine"] = 4] = "_TagLine";
|
|
32
|
+
RuleType[RuleType["_FeatureLine"] = 5] = "_FeatureLine";
|
|
33
|
+
RuleType[RuleType["_RuleLine"] = 6] = "_RuleLine";
|
|
34
|
+
RuleType[RuleType["_BackgroundLine"] = 7] = "_BackgroundLine";
|
|
35
|
+
RuleType[RuleType["_ScenarioLine"] = 8] = "_ScenarioLine";
|
|
36
|
+
RuleType[RuleType["_ExamplesLine"] = 9] = "_ExamplesLine";
|
|
37
|
+
RuleType[RuleType["_StepLine"] = 10] = "_StepLine";
|
|
38
|
+
RuleType[RuleType["_DocStringSeparator"] = 11] = "_DocStringSeparator";
|
|
39
|
+
RuleType[RuleType["_TableRow"] = 12] = "_TableRow";
|
|
40
|
+
RuleType[RuleType["_Language"] = 13] = "_Language";
|
|
41
|
+
RuleType[RuleType["_Other"] = 14] = "_Other";
|
|
42
|
+
RuleType[RuleType["GherkinDocument"] = 15] = "GherkinDocument";
|
|
43
|
+
RuleType[RuleType["Feature"] = 16] = "Feature";
|
|
44
|
+
RuleType[RuleType["FeatureHeader"] = 17] = "FeatureHeader";
|
|
45
|
+
RuleType[RuleType["Rule"] = 18] = "Rule";
|
|
46
|
+
RuleType[RuleType["RuleHeader"] = 19] = "RuleHeader";
|
|
47
|
+
RuleType[RuleType["Background"] = 20] = "Background";
|
|
48
|
+
RuleType[RuleType["ScenarioDefinition"] = 21] = "ScenarioDefinition";
|
|
49
|
+
RuleType[RuleType["Scenario"] = 22] = "Scenario";
|
|
50
|
+
RuleType[RuleType["ExamplesDefinition"] = 23] = "ExamplesDefinition";
|
|
51
|
+
RuleType[RuleType["Examples"] = 24] = "Examples";
|
|
52
|
+
RuleType[RuleType["ExamplesTable"] = 25] = "ExamplesTable";
|
|
53
|
+
RuleType[RuleType["Step"] = 26] = "Step";
|
|
54
|
+
RuleType[RuleType["StepArg"] = 27] = "StepArg";
|
|
55
|
+
RuleType[RuleType["DataTable"] = 28] = "DataTable";
|
|
56
|
+
RuleType[RuleType["DocString"] = 29] = "DocString";
|
|
57
|
+
RuleType[RuleType["Tags"] = 30] = "Tags";
|
|
58
|
+
RuleType[RuleType["DescriptionHelper"] = 31] = "DescriptionHelper";
|
|
59
|
+
RuleType[RuleType["Description"] = 32] = "Description";
|
|
60
|
+
})(RuleType || (RuleType = {}));
|
|
61
|
+
var AttachmentContentEncoding;
|
|
62
|
+
(function(AttachmentContentEncoding) {
|
|
63
|
+
AttachmentContentEncoding["IDENTITY"] = "IDENTITY";
|
|
64
|
+
AttachmentContentEncoding["BASE64"] = "BASE64";
|
|
65
|
+
})(AttachmentContentEncoding || (AttachmentContentEncoding = {}));
|
|
66
|
+
var HookType;
|
|
67
|
+
(function(HookType) {
|
|
68
|
+
HookType["BEFORE_TEST_RUN"] = "BEFORE_TEST_RUN";
|
|
69
|
+
HookType["AFTER_TEST_RUN"] = "AFTER_TEST_RUN";
|
|
70
|
+
HookType["BEFORE_TEST_CASE"] = "BEFORE_TEST_CASE";
|
|
71
|
+
HookType["AFTER_TEST_CASE"] = "AFTER_TEST_CASE";
|
|
72
|
+
HookType["BEFORE_TEST_STEP"] = "BEFORE_TEST_STEP";
|
|
73
|
+
HookType["AFTER_TEST_STEP"] = "AFTER_TEST_STEP";
|
|
74
|
+
})(HookType || (HookType = {}));
|
|
75
|
+
var PickleStepType;
|
|
76
|
+
(function(PickleStepType) {
|
|
77
|
+
PickleStepType["UNKNOWN"] = "Unknown";
|
|
78
|
+
PickleStepType["CONTEXT"] = "Context";
|
|
79
|
+
PickleStepType["ACTION"] = "Action";
|
|
80
|
+
PickleStepType["OUTCOME"] = "Outcome";
|
|
81
|
+
})(PickleStepType || (PickleStepType = {}));
|
|
82
|
+
var SourceMediaType;
|
|
83
|
+
(function(SourceMediaType) {
|
|
84
|
+
SourceMediaType["TEXT_X_CUCUMBER_GHERKIN_PLAIN"] = "text/x.cucumber.gherkin+plain";
|
|
85
|
+
SourceMediaType["TEXT_X_CUCUMBER_GHERKIN_MARKDOWN"] = "text/x.cucumber.gherkin+markdown";
|
|
86
|
+
})(SourceMediaType || (SourceMediaType = {}));
|
|
87
|
+
var StepDefinitionPatternType;
|
|
88
|
+
(function(StepDefinitionPatternType) {
|
|
89
|
+
StepDefinitionPatternType["CUCUMBER_EXPRESSION"] = "CUCUMBER_EXPRESSION";
|
|
90
|
+
StepDefinitionPatternType["REGULAR_EXPRESSION"] = "REGULAR_EXPRESSION";
|
|
91
|
+
})(StepDefinitionPatternType || (StepDefinitionPatternType = {}));
|
|
92
|
+
var StepKeywordType;
|
|
93
|
+
(function(StepKeywordType) {
|
|
94
|
+
StepKeywordType["UNKNOWN"] = "Unknown";
|
|
95
|
+
StepKeywordType["CONTEXT"] = "Context";
|
|
96
|
+
StepKeywordType["ACTION"] = "Action";
|
|
97
|
+
StepKeywordType["OUTCOME"] = "Outcome";
|
|
98
|
+
StepKeywordType["CONJUNCTION"] = "Conjunction";
|
|
99
|
+
})(StepKeywordType || (StepKeywordType = {}));
|
|
100
|
+
var TestStepResultStatus;
|
|
101
|
+
(function(TestStepResultStatus) {
|
|
102
|
+
TestStepResultStatus["UNKNOWN"] = "UNKNOWN";
|
|
103
|
+
TestStepResultStatus["PASSED"] = "PASSED";
|
|
104
|
+
TestStepResultStatus["SKIPPED"] = "SKIPPED";
|
|
105
|
+
TestStepResultStatus["PENDING"] = "PENDING";
|
|
106
|
+
TestStepResultStatus["UNDEFINED"] = "UNDEFINED";
|
|
107
|
+
TestStepResultStatus["AMBIGUOUS"] = "AMBIGUOUS";
|
|
108
|
+
TestStepResultStatus["FAILED"] = "FAILED";
|
|
109
|
+
})(TestStepResultStatus || (TestStepResultStatus = {}));
|
|
110
|
+
var KeywordPrefix;
|
|
111
|
+
(function(KeywordPrefix) {
|
|
112
|
+
KeywordPrefix["BULLET"] = "^(\\s*[*+-]\\s*)";
|
|
113
|
+
KeywordPrefix["HEADER"] = "^(#{1,6}\\s)";
|
|
114
|
+
})(KeywordPrefix || (KeywordPrefix = {}));
|
|
115
|
+
StepKeywordType.UNKNOWN, PickleStepType.UNKNOWN, StepKeywordType.CONTEXT, PickleStepType.CONTEXT, StepKeywordType.ACTION, PickleStepType.ACTION, StepKeywordType.OUTCOME, PickleStepType.OUTCOME, StepKeywordType.CONJUNCTION;
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region skills/touch-set-correction/scripts/touch-set-correction.mts
|
|
118
|
+
function stripTrailingSlash$1(root) {
|
|
119
|
+
return root.endsWith("/") ? root.slice(0, -1) : root;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* fileToNode — capability-first work-area recovery. For each project's root, if `path` sits under
|
|
123
|
+
* `root/`, the CAPABILITY is the first path segment after the matched root; the node is
|
|
124
|
+
* `project/capability`. Matches the LONGEST matching root prefix across every project + root (so a
|
|
125
|
+
* deeper root like `plugins/sdd/skills` wins over a shallower `plugins/sdd`, when both are
|
|
126
|
+
* declared). Returns null when no root matches — the file is unmapped.
|
|
127
|
+
*/
|
|
128
|
+
function fileToNode(path, projects) {
|
|
129
|
+
let best = null;
|
|
130
|
+
for (const p of projects) for (const rawRoot of p.roots) {
|
|
131
|
+
const root = stripTrailingSlash$1(rawRoot);
|
|
132
|
+
const prefix = root === "" ? "" : `${root}/`;
|
|
133
|
+
if (prefix === "" || !path.startsWith(prefix)) continue;
|
|
134
|
+
const capability = path.slice(prefix.length).split("/")[0];
|
|
135
|
+
if (!capability) continue;
|
|
136
|
+
if (best === null || prefix.length > best.rootLen) best = {
|
|
137
|
+
project: p.project,
|
|
138
|
+
capability,
|
|
139
|
+
rootLen: prefix.length
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return best ? `${best.project}/${best.capability}` : null;
|
|
143
|
+
}
|
|
144
|
+
const HERE = fileURLToPath(import.meta.url);
|
|
145
|
+
const ENGINE_EXT = HERE.endsWith(".mts") ? ".mts" : ".bundle.mjs";
|
|
146
|
+
join(dirname(HERE), "..", "..", "resolve-governances", "scripts", `resolve-governances${ENGINE_EXT}`);
|
|
147
|
+
const DISCOVER_SPECS_PATH = join(dirname(HERE), "..", "..", "discover-specs", "scripts", `discover-specs${ENGINE_EXT}`);
|
|
148
|
+
/** Best-effort project-layout discovery via `discover-specs`: each project's spec-path plus an
|
|
149
|
+
* impl-root convention (`plugins/<p>` → also `plugins/<p>/skills`; `packages/<p>` → also
|
|
150
|
+
* `packages/<p>/src`; else the project-path itself). `--layout` (CLI) overrides this entirely. On
|
|
151
|
+
* any failure returns []. */
|
|
152
|
+
function discoverLayouts(root, cwd) {
|
|
153
|
+
try {
|
|
154
|
+
const out = execFileSync("node", [
|
|
155
|
+
DISCOVER_SPECS_PATH,
|
|
156
|
+
"--root",
|
|
157
|
+
root,
|
|
158
|
+
"--format",
|
|
159
|
+
"json"
|
|
160
|
+
], {
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
cwd,
|
|
163
|
+
stdio: [
|
|
164
|
+
"ignore",
|
|
165
|
+
"pipe",
|
|
166
|
+
"ignore"
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
return JSON.parse(out).map((s) => {
|
|
170
|
+
const roots = [s.path];
|
|
171
|
+
const projectPath = s.projectPath ?? "";
|
|
172
|
+
const pluginsMatch = /^plugins\/([^/]+)$/.exec(projectPath);
|
|
173
|
+
const packagesMatch = /^packages\/([^/]+)$/.exec(projectPath);
|
|
174
|
+
if (pluginsMatch) roots.push(`plugins/${pluginsMatch[1]}/skills`);
|
|
175
|
+
else if (packagesMatch) roots.push(`packages/${packagesMatch[1]}/src`);
|
|
176
|
+
else if (projectPath) roots.push(projectPath);
|
|
177
|
+
return {
|
|
178
|
+
project: s.name,
|
|
179
|
+
roots
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (process.argv[1] && false);
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region skills/blast-estimate/scripts/blast-estimate.mts
|
|
189
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
190
|
+
"node_modules",
|
|
191
|
+
".git",
|
|
192
|
+
"dist",
|
|
193
|
+
".turbo",
|
|
194
|
+
".next",
|
|
195
|
+
"coverage"
|
|
196
|
+
]);
|
|
197
|
+
const SENSITIVE_PATHS_FILE = ".agents/sdd/sensitive-paths.toml";
|
|
198
|
+
/** Every repo-relative file path under `dir` (recursively), skipping build/vendor noise. Paths are
|
|
199
|
+
* made relative with `path.relative`, never string-sliced: `join('.', x)` normalizes the `./` away,
|
|
200
|
+
* so slicing by `root.length` silently corrupts every path under a RELATIVE root (`--root .`, the
|
|
201
|
+
* CLI's default) while working fine under an absolute one. */
|
|
202
|
+
function walkFiles(dir, root, out) {
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
211
|
+
const full = join(dir, entry.name);
|
|
212
|
+
if (entry.isDirectory()) walkFiles(full, root, out);
|
|
213
|
+
else out.push(relative(root, full).replace(/\\/g, "/"));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* discoverWorkAreas — the corpus's work areas, grouped by node id, over the DECLARED layouts. Walks
|
|
218
|
+
* every root of every project, maps each file through touch-set-correction's pure `fileToNode`, and
|
|
219
|
+
* groups by the node it names. Because a node's roots include BOTH its spec root and its impl root,
|
|
220
|
+
* one node's file set spans both trees — which is exactly what makes fan-in measure real dependency
|
|
221
|
+
* rather than spec-prose cross-reference. A file that maps to no node (`fileToNode` returns null —
|
|
222
|
+
* the existing "unmapped" concept) is dropped from the area map, never invented into an atom.
|
|
223
|
+
*
|
|
224
|
+
* Values are repo-relative paths; `root` is the repo root they are resolved against.
|
|
225
|
+
*/
|
|
226
|
+
function discoverWorkAreas(layouts, root) {
|
|
227
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
228
|
+
const seen = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const layout of layouts) for (const rawRoot of layout.roots) {
|
|
230
|
+
const rel = rawRoot.replace(/\/+$/, "");
|
|
231
|
+
const files = [];
|
|
232
|
+
walkFiles(join(root, rel), root, files);
|
|
233
|
+
for (const path of files) {
|
|
234
|
+
if (seen.has(path)) continue;
|
|
235
|
+
seen.add(path);
|
|
236
|
+
const node = fileToNode(path, layouts);
|
|
237
|
+
if (node === null) continue;
|
|
238
|
+
const bucket = byNode.get(node);
|
|
239
|
+
if (bucket) bucket.push(path);
|
|
240
|
+
else byNode.set(node, [path]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return byNode;
|
|
244
|
+
}
|
|
245
|
+
function escapeRegExp(s) {
|
|
246
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
247
|
+
}
|
|
248
|
+
/** Left boundary: a reference must not start mid-token. */
|
|
249
|
+
const LB = "(?:^|[^\\w/:.-])";
|
|
250
|
+
function stripTrailingSlash(s) {
|
|
251
|
+
return s.replace(/\/+$/, "");
|
|
252
|
+
}
|
|
253
|
+
/** The reference forms that name `nodeId` unambiguously from ANYWHERE (each is project-qualified):
|
|
254
|
+
* the bare id (`sdd/spec-gate`), the skill-style ref (`sdd:spec-gate`), and a path under any of the
|
|
255
|
+
* project's declared roots, at any nesting depth (`plugins/sdd/skills/spec-gate/`,
|
|
256
|
+
* `.agents/specs/sdd/authoring/spec-gate/`). */
|
|
257
|
+
function globalReferencePattern(project, capability, roots) {
|
|
258
|
+
const alts = [`${LB}${escapeRegExp(`${project}/${capability}`)}(?![\\w-])`, `${LB}${escapeRegExp(`${project}:${capability}`)}(?![\\w-])`];
|
|
259
|
+
for (const root of roots) alts.push(`${escapeRegExp(stripTrailingSlash(root))}/(?:[\\w.-]+/)*${escapeRegExp(capability)}/`);
|
|
260
|
+
return new RegExp(alts.join("|"));
|
|
261
|
+
}
|
|
262
|
+
/** The relative-link form (`../spec-gate/`, `../../authoring/spec-gate/`) — a sibling-node link. It
|
|
263
|
+
* carries no project, so it is only counted from a file in the SAME project; otherwise two projects
|
|
264
|
+
* sharing a capability name (`manage`, `design`) would cross-credit each other's fan-in. */
|
|
265
|
+
function relativeReferencePattern(capability) {
|
|
266
|
+
return new RegExp(`(?:\\.\\./)+(?:[\\w.-]+/)*${escapeRegExp(capability)}/`);
|
|
267
|
+
}
|
|
268
|
+
function projectRoots(project, layouts) {
|
|
269
|
+
return layouts.filter((l) => l.project === project).flatMap((l) => l.roots);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* computeFanInMap — centrality for the requested areas: area A's fan-in is the number of OTHER areas
|
|
273
|
+
* holding at least one file that REFERENCES A in any of the forms the corpus actually uses (see
|
|
274
|
+
* `globalReferencePattern` / `relativeReferencePattern`). Because a node's file set spans its spec
|
|
275
|
+
* AND impl roots, a reference from an implementation file counts exactly as spec prose does — fan-in
|
|
276
|
+
* measures the project's real lean on an area. An area's own files never count toward its own fan-in.
|
|
277
|
+
*
|
|
278
|
+
* `targets` defaults to every known area; passing only the areas actually being scored keeps the CLI
|
|
279
|
+
* cheap (a one-area touch-set costs 1 pattern × N files instead of 229 × N). Each file is read at
|
|
280
|
+
* most once, and an owner already credited for a target is skipped.
|
|
281
|
+
*/
|
|
282
|
+
function computeFanInMap(byNode, root, layouts, targets) {
|
|
283
|
+
const ids = targets ?? [...byNode.keys()];
|
|
284
|
+
const specs = ids.map((id) => {
|
|
285
|
+
const slash = id.indexOf("/");
|
|
286
|
+
const project = id.slice(0, slash);
|
|
287
|
+
const capability = id.slice(slash + 1);
|
|
288
|
+
return {
|
|
289
|
+
id,
|
|
290
|
+
project,
|
|
291
|
+
global: globalReferencePattern(project, capability, projectRoots(project, layouts)),
|
|
292
|
+
relative: relativeReferencePattern(capability)
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
const referencers = new Map(ids.map((id) => [id, /* @__PURE__ */ new Set()]));
|
|
296
|
+
for (const [owner, files] of byNode) {
|
|
297
|
+
const ownerProject = projectOf(owner);
|
|
298
|
+
for (const rel of files) {
|
|
299
|
+
let text;
|
|
300
|
+
try {
|
|
301
|
+
text = readFileSync(join(root, rel), "utf8");
|
|
302
|
+
} catch {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
for (const spec of specs) {
|
|
306
|
+
if (spec.id === owner) continue;
|
|
307
|
+
const seen = referencers.get(spec.id);
|
|
308
|
+
if (seen?.has(owner)) continue;
|
|
309
|
+
if (spec.global.test(text) || ownerProject === spec.project && spec.relative.test(text)) seen?.add(owner);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return new Map(ids.map((id) => [id, referencers.get(id)?.size ?? 0]));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Parses the opt-in `.agents/sdd/sensitive-paths.toml`: a `sensitive = [ "id", ... ]` string array.
|
|
317
|
+
*
|
|
318
|
+
* LINE-anchored and lenient, matching manage-spec-anchors' `anchors = [ … ]` parser byte for byte in
|
|
319
|
+
* shape — a leading comment, a trailing comment, or a neighbouring key are ordinary TOML and must
|
|
320
|
+
* parse. An earlier whole-file-anchored regex rejected all three, so a perfectly valid file that
|
|
321
|
+
* marked an area computed no level at all.
|
|
322
|
+
*
|
|
323
|
+
* An EMPTY file is `[]` (a real "nothing marked" declaration). A file with no `sensitive` array at
|
|
324
|
+
* all throws — that is genuinely malformed, and an unreadable marking is not evidence of no
|
|
325
|
+
* markings.
|
|
326
|
+
*/
|
|
327
|
+
function parseSensitivePaths(text) {
|
|
328
|
+
if (text.trim() === "") return [];
|
|
329
|
+
const m = /(^|\n)\s*sensitive\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
330
|
+
if (!m) throw new Error("expected a `sensitive = [...]` array");
|
|
331
|
+
const out = [];
|
|
332
|
+
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
|
|
333
|
+
return out.filter((s) => s !== "");
|
|
334
|
+
}
|
|
335
|
+
/** Reads the opt-in sensitive-paths file. **Absent** (ENOENT) = `{ ok: true, marked: [] }` — no area
|
|
336
|
+
* is sensitive, and that is NOT an error. Anything else — present-but-unparseable, unreadable
|
|
337
|
+
* (permissions), or not a regular file — = `{ ok: false, error }`, and the caller computes no level.
|
|
338
|
+
*
|
|
339
|
+
* ONLY ENOENT is benign. A bare `catch` here would swallow EACCES and a directory-at-the-path into
|
|
340
|
+
* "no markings", which fails in the DANGEROUS direction: the estimate silently UNDER-calls blast on
|
|
341
|
+
* exactly the areas a project took the trouble to mark. A read that cannot classify must fail loud,
|
|
342
|
+
* never default to the safe-looking answer — the same duty the absent-vs-unparseable scenarios draw:
|
|
343
|
+
* an unreadable marking is not evidence of no markings. */
|
|
344
|
+
function readSensitivePaths(corpusRoot) {
|
|
345
|
+
const file = join(corpusRoot, SENSITIVE_PATHS_FILE);
|
|
346
|
+
let text;
|
|
347
|
+
try {
|
|
348
|
+
if (!statSync(file).isFile()) return {
|
|
349
|
+
ok: false,
|
|
350
|
+
error: `${SENSITIVE_PATHS_FILE} is not a regular file`
|
|
351
|
+
};
|
|
352
|
+
text = readFileSync(file, "utf8");
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err.code === "ENOENT") return {
|
|
355
|
+
ok: true,
|
|
356
|
+
marked: []
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
error: `${SENSITIVE_PATHS_FILE} is unreadable: ${err.message}`
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
marked: parseSensitivePaths(text)
|
|
367
|
+
};
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
error: `${SENSITIVE_PATHS_FILE} is unreadable: ${err.message}`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function countScore(n) {
|
|
376
|
+
if (n <= 1) return 0;
|
|
377
|
+
if (n <= 3) return 1;
|
|
378
|
+
return 3;
|
|
379
|
+
}
|
|
380
|
+
const COVERAGE_SCORE = 3;
|
|
381
|
+
function centralityScore(fanIn) {
|
|
382
|
+
if (fanIn <= 0) return 0;
|
|
383
|
+
if (fanIn <= 2) return 1;
|
|
384
|
+
if (fanIn <= 6) return 2;
|
|
385
|
+
return 3;
|
|
386
|
+
}
|
|
387
|
+
const SENSITIVITY_SCORE = 2;
|
|
388
|
+
function levelFromScore(score) {
|
|
389
|
+
if (score >= 3) return "high";
|
|
390
|
+
if (score >= 1) return "medium";
|
|
391
|
+
return "low";
|
|
392
|
+
}
|
|
393
|
+
/** The project a work area belongs to — the first segment of its `project/capability` id. */
|
|
394
|
+
function projectOf(nodeId) {
|
|
395
|
+
return nodeId.split("/")[0];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* projectsCoveredEntirely — the touched projects whose EVERY work area is in the touch-set, among
|
|
399
|
+
* projects holding >= 2 work areas. Coverage is relative reach: it asks what fraction of a project
|
|
400
|
+
* is disturbed, where `count` only asks how many areas in absolute terms. A project with a single
|
|
401
|
+
* work area never qualifies (see the guard note above).
|
|
402
|
+
*
|
|
403
|
+
* "Every work area of its project" means every area the LAYOUTS declare — `byNode` is the discovered
|
|
404
|
+
* area map keyed by node id, so a project's area set is its nodes across all of its declared roots
|
|
405
|
+
* (spec + impl), not whatever a directory walk happened to find.
|
|
406
|
+
*/
|
|
407
|
+
function projectsCoveredEntirely(resolved, byNode) {
|
|
408
|
+
const areasByProject = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const node of byNode.keys()) {
|
|
410
|
+
const p = projectOf(node);
|
|
411
|
+
const areas = areasByProject.get(p);
|
|
412
|
+
if (areas) areas.push(node);
|
|
413
|
+
else areasByProject.set(p, [node]);
|
|
414
|
+
}
|
|
415
|
+
const resolvedSet = new Set(resolved);
|
|
416
|
+
const covered = [];
|
|
417
|
+
for (const project of new Set(resolved.map(projectOf))) {
|
|
418
|
+
const areas = areasByProject.get(project) ?? [];
|
|
419
|
+
if (areas.length < 2) continue;
|
|
420
|
+
if (areas.every((a) => resolvedSet.has(a))) covered.push(project);
|
|
421
|
+
}
|
|
422
|
+
return covered.sort();
|
|
423
|
+
}
|
|
424
|
+
/** The pure scoring step, once `count`/`maxFanIn`/`sensitiveAreas`/`projectWide` are known. Exported
|
|
425
|
+
* so the ordering-property scenarios (hub-vs-leaf, marked-vs-unmarked) can be driven directly. */
|
|
426
|
+
function scoreBlast(count, maxFanIn, sensitiveAreas, projectWide = []) {
|
|
427
|
+
return {
|
|
428
|
+
level: levelFromScore(Math.max(countScore(count), projectWide.length > 0 ? COVERAGE_SCORE : 0) + centralityScore(maxFanIn) + (sensitiveAreas.length > 0 ? SENSITIVITY_SCORE : 0)),
|
|
429
|
+
reasons: {
|
|
430
|
+
count,
|
|
431
|
+
maxFanIn,
|
|
432
|
+
sensitiveAreas,
|
|
433
|
+
projectWide
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const ORDER = [
|
|
438
|
+
"low",
|
|
439
|
+
"medium",
|
|
440
|
+
"high"
|
|
441
|
+
];
|
|
442
|
+
/** Lines the computed level up against the hand-typed declared one. An absent or `unknown` declared
|
|
443
|
+
* blast is not an error — it is `no-declared`, and the computed level still stands on its own. */
|
|
444
|
+
function lineUp(computed, declared) {
|
|
445
|
+
if (declared === void 0 || declared === "unknown") return {
|
|
446
|
+
outcome: "no-declared",
|
|
447
|
+
computed
|
|
448
|
+
};
|
|
449
|
+
const di = ORDER.indexOf(declared);
|
|
450
|
+
const ci = ORDER.indexOf(computed);
|
|
451
|
+
if (di === ci) return {
|
|
452
|
+
outcome: "agrees",
|
|
453
|
+
computed,
|
|
454
|
+
declared
|
|
455
|
+
};
|
|
456
|
+
if (di < ci) return {
|
|
457
|
+
outcome: "under-called",
|
|
458
|
+
computed,
|
|
459
|
+
declared
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
outcome: "over-called",
|
|
463
|
+
computed,
|
|
464
|
+
declared
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* estimateBlast — the whole derivation, over INJECTED layouts. Discovers the corpus's work areas by
|
|
469
|
+
* mapping every file under every declared root through touch-set-correction's pure `fileToNode`,
|
|
470
|
+
* resolves the touch-set against those areas (unresolved areas are surfaced, never dropped), reads
|
|
471
|
+
* the opt-in sensitive-paths file (fails loud on a malformed one — computed/reasons/lineUp all come
|
|
472
|
+
* back null, `error` names why), and — when at least one area resolved — scores
|
|
473
|
+
* breadth × centrality × sensitivity and lines the result up against `opts.declared`. A touch-set
|
|
474
|
+
* that resolves to zero known areas (including the empty touch-set) computes `unknown`: nothing
|
|
475
|
+
* touched is not evidence of low reach.
|
|
476
|
+
*
|
|
477
|
+
* Layouts are INJECTED rather than discovered here: `fileToNode` is pure, so tests construct layouts
|
|
478
|
+
* as fixtures over a constructed corpus (the frozen suite's "never the live corpus" preamble holds),
|
|
479
|
+
* while the CLI sources them from `discoverLayouts`.
|
|
480
|
+
*/
|
|
481
|
+
function estimateBlast(touchSet, layouts, opts = {}) {
|
|
482
|
+
const root = opts.root ?? ".";
|
|
483
|
+
const byNode = discoverWorkAreas(layouts, root);
|
|
484
|
+
const known = new Set(byNode.keys());
|
|
485
|
+
const unique = [...new Set(touchSet)];
|
|
486
|
+
const resolved = unique.filter((a) => known.has(a));
|
|
487
|
+
const unresolved = unique.filter((a) => !known.has(a));
|
|
488
|
+
const sensitive = readSensitivePaths(root);
|
|
489
|
+
if (!sensitive.ok) return {
|
|
490
|
+
touchSet,
|
|
491
|
+
resolved,
|
|
492
|
+
unresolved,
|
|
493
|
+
computed: null,
|
|
494
|
+
reasons: null,
|
|
495
|
+
lineUp: null,
|
|
496
|
+
error: sensitive.error
|
|
497
|
+
};
|
|
498
|
+
if (resolved.length === 0) return {
|
|
499
|
+
touchSet,
|
|
500
|
+
resolved,
|
|
501
|
+
unresolved,
|
|
502
|
+
computed: "unknown",
|
|
503
|
+
reasons: {
|
|
504
|
+
count: 0,
|
|
505
|
+
maxFanIn: 0,
|
|
506
|
+
sensitiveAreas: [],
|
|
507
|
+
projectWide: []
|
|
508
|
+
},
|
|
509
|
+
lineUp: null
|
|
510
|
+
};
|
|
511
|
+
const markedSet = new Set(sensitive.marked);
|
|
512
|
+
const fanInMap = computeFanInMap(byNode, root, layouts, resolved);
|
|
513
|
+
const maxFanIn = Math.max(...resolved.map((a) => fanInMap.get(a) ?? 0));
|
|
514
|
+
const sensitiveAreas = resolved.filter((a) => markedSet.has(a));
|
|
515
|
+
const projectWide = projectsCoveredEntirely(resolved, byNode);
|
|
516
|
+
const { level, reasons } = scoreBlast(resolved.length, maxFanIn, sensitiveAreas, projectWide);
|
|
517
|
+
return {
|
|
518
|
+
touchSet,
|
|
519
|
+
resolved,
|
|
520
|
+
unresolved,
|
|
521
|
+
computed: level,
|
|
522
|
+
reasons,
|
|
523
|
+
lineUp: lineUp(level, opts.declared)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function toonQuote(v) {
|
|
527
|
+
if (v === "" || /[",;]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
528
|
+
return v;
|
|
529
|
+
}
|
|
530
|
+
function renderResultToon(r) {
|
|
531
|
+
if (r.error) return [`blast-estimate: error="${r.error}"`, `unresolved[${r.unresolved.length}]: ${r.unresolved.map(toonQuote).join(";")}`].join("\n");
|
|
532
|
+
const lines = [];
|
|
533
|
+
const lu = r.lineUp;
|
|
534
|
+
const lineUpPart = lu ? lu.outcome === "no-declared" ? "lineup=no-declared" : `lineup=${lu.outcome}(declared=${lu.declared})` : "lineup=n/a";
|
|
535
|
+
lines.push(`blast-estimate: computed=${r.computed} ${lineUpPart}`);
|
|
536
|
+
if (r.reasons) lines.push(`reasons: count=${r.reasons.count} maxFanIn=${r.reasons.maxFanIn} sensitiveAreas=[${r.reasons.sensitiveAreas.join(";")}] projectWide=[${r.reasons.projectWide.join(";")}]`);
|
|
537
|
+
lines.push(`resolved[${r.resolved.length}]: ${r.resolved.map(toonQuote).join(";")}`);
|
|
538
|
+
lines.push(`unresolved[${r.unresolved.length}]: ${r.unresolved.map(toonQuote).join(";")}`);
|
|
539
|
+
return lines.join("\n");
|
|
540
|
+
}
|
|
541
|
+
function flag(argv, name) {
|
|
542
|
+
const i = argv.indexOf(name);
|
|
543
|
+
return i === -1 ? void 0 : argv[i + 1];
|
|
544
|
+
}
|
|
545
|
+
function allFlags(argv, name) {
|
|
546
|
+
const out = [];
|
|
547
|
+
for (let i = 0; i < argv.length; i++) if (argv[i] === name && argv[i + 1] !== void 0) out.push(argv[i + 1]);
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
function splitCsv(v) {
|
|
551
|
+
if (v === void 0 || v === "") return [];
|
|
552
|
+
return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
553
|
+
}
|
|
554
|
+
/** Parses one `--layout '<project>:<root1>,<root2>'` flag value into a ProjectLayout (the same shape
|
|
555
|
+
* and flag touch-set-correction accepts, so the two tools take identical layout overrides). */
|
|
556
|
+
function parseLayoutFlag(value) {
|
|
557
|
+
const idx = value.indexOf(":");
|
|
558
|
+
if (idx === -1) return null;
|
|
559
|
+
const project = value.slice(0, idx).trim();
|
|
560
|
+
const roots = splitCsv(value.slice(idx + 1));
|
|
561
|
+
if (project === "" || roots.length === 0) return null;
|
|
562
|
+
return {
|
|
563
|
+
project,
|
|
564
|
+
roots
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** The legal `--declared` values, exactly. Returns `null` for anything else so the CLI can fail loud
|
|
568
|
+
* rather than let an unranked value masquerade as "below everything". Case-sensitive on purpose: the
|
|
569
|
+
* store writes these lowercase, and quietly accepting `High` would invite a second spelling. */
|
|
570
|
+
function parseDeclared(raw) {
|
|
571
|
+
return raw === "low" || raw === "medium" || raw === "high" || raw === "unknown" ? raw : null;
|
|
572
|
+
}
|
|
573
|
+
function main(argv) {
|
|
574
|
+
const root = flag(argv, "--root") ?? ".";
|
|
575
|
+
const touchSet = splitCsv(flag(argv, "--touch-set"));
|
|
576
|
+
const declaredRaw = flag(argv, "--declared");
|
|
577
|
+
const declared = declaredRaw === void 0 ? void 0 : parseDeclared(declaredRaw);
|
|
578
|
+
if (declared === null) {
|
|
579
|
+
process.stderr.write(`blast-estimate: --declared must be one of low, medium, high, unknown (got "${declaredRaw}")\n`);
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
const format = flag(argv, "--format") === "json" ? "json" : "toon";
|
|
583
|
+
const layoutFlags = allFlags(argv, "--layout");
|
|
584
|
+
const result = estimateBlast(touchSet, layoutFlags.length > 0 ? layoutFlags.map(parseLayoutFlag).filter((l) => l !== null) : discoverLayouts(root, root), {
|
|
585
|
+
root,
|
|
586
|
+
declared
|
|
587
|
+
});
|
|
588
|
+
process.stdout.write(`${format === "json" ? JSON.stringify(result, null, 2) : renderResultToon(result)}\n`);
|
|
589
|
+
return result.error ? 1 : 0;
|
|
590
|
+
}
|
|
591
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
592
|
+
//#endregion
|
|
593
|
+
export { computeFanInMap, discoverWorkAreas, estimateBlast, levelFromScore, lineUp, main, parseDeclared, parseLayoutFlag, parseSensitivePaths, projectsCoveredEntirely, readSensitivePaths, renderResultToon, scoreBlast };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
|
|
2
|
+
// A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
|
|
3
|
+
// will not strip types, so it runs the self-contained bundle built for the package.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const self = fileURLToPath(import.meta.url)
|
|
8
|
+
const installed = /[\\/]node_modules[\\/]/.test(self)
|
|
9
|
+
const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
|
|
10
|
+
const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
11
|
+
if (run.error) throw run.error
|
|
12
|
+
process.exitCode = run.status ?? 1
|
|
@@ -52,7 +52,7 @@ A marker with no reason excuses nothing. There is no allow-list file: a real mis
|
|
|
52
52
|
## Run it
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
node "<skill>/scripts/check-field-mandates.
|
|
55
|
+
node "<skill>/scripts/check-field-mandates.mjs" [--root <dir>]
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
`--root` defaults to the working directory. A finding always exits non-zero, as does a root that is
|