@team-attention/hoyeon-cli 1.0.1 → 1.1.1
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/dist/cli.js +412 -37
- package/package.json +6 -5
- package/schemas/dev-spec-v5.schema.json +20 -0
package/dist/cli.js
CHANGED
|
@@ -402,11 +402,11 @@ var require_codegen = __commonJS({
|
|
|
402
402
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
403
403
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
404
404
|
}
|
|
405
|
-
optimizeNames(names,
|
|
405
|
+
optimizeNames(names, constants2) {
|
|
406
406
|
if (!names[this.name.str])
|
|
407
407
|
return;
|
|
408
408
|
if (this.rhs)
|
|
409
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
409
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
410
410
|
return this;
|
|
411
411
|
}
|
|
412
412
|
get names() {
|
|
@@ -423,10 +423,10 @@ var require_codegen = __commonJS({
|
|
|
423
423
|
render({ _n }) {
|
|
424
424
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
425
425
|
}
|
|
426
|
-
optimizeNames(names,
|
|
426
|
+
optimizeNames(names, constants2) {
|
|
427
427
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
428
428
|
return;
|
|
429
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
429
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
430
430
|
return this;
|
|
431
431
|
}
|
|
432
432
|
get names() {
|
|
@@ -487,8 +487,8 @@ var require_codegen = __commonJS({
|
|
|
487
487
|
optimizeNodes() {
|
|
488
488
|
return `${this.code}` ? this : void 0;
|
|
489
489
|
}
|
|
490
|
-
optimizeNames(names,
|
|
491
|
-
this.code = optimizeExpr(this.code, names,
|
|
490
|
+
optimizeNames(names, constants2) {
|
|
491
|
+
this.code = optimizeExpr(this.code, names, constants2);
|
|
492
492
|
return this;
|
|
493
493
|
}
|
|
494
494
|
get names() {
|
|
@@ -517,12 +517,12 @@ var require_codegen = __commonJS({
|
|
|
517
517
|
}
|
|
518
518
|
return nodes.length > 0 ? this : void 0;
|
|
519
519
|
}
|
|
520
|
-
optimizeNames(names,
|
|
520
|
+
optimizeNames(names, constants2) {
|
|
521
521
|
const { nodes } = this;
|
|
522
522
|
let i = nodes.length;
|
|
523
523
|
while (i--) {
|
|
524
524
|
const n = nodes[i];
|
|
525
|
-
if (n.optimizeNames(names,
|
|
525
|
+
if (n.optimizeNames(names, constants2))
|
|
526
526
|
continue;
|
|
527
527
|
subtractNames(names, n.names);
|
|
528
528
|
nodes.splice(i, 1);
|
|
@@ -575,12 +575,12 @@ var require_codegen = __commonJS({
|
|
|
575
575
|
return void 0;
|
|
576
576
|
return this;
|
|
577
577
|
}
|
|
578
|
-
optimizeNames(names,
|
|
578
|
+
optimizeNames(names, constants2) {
|
|
579
579
|
var _a;
|
|
580
|
-
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
581
|
-
if (!(super.optimizeNames(names,
|
|
580
|
+
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
|
|
581
|
+
if (!(super.optimizeNames(names, constants2) || this.else))
|
|
582
582
|
return;
|
|
583
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
583
|
+
this.condition = optimizeExpr(this.condition, names, constants2);
|
|
584
584
|
return this;
|
|
585
585
|
}
|
|
586
586
|
get names() {
|
|
@@ -603,10 +603,10 @@ var require_codegen = __commonJS({
|
|
|
603
603
|
render(opts) {
|
|
604
604
|
return `for(${this.iteration})` + super.render(opts);
|
|
605
605
|
}
|
|
606
|
-
optimizeNames(names,
|
|
607
|
-
if (!super.optimizeNames(names,
|
|
606
|
+
optimizeNames(names, constants2) {
|
|
607
|
+
if (!super.optimizeNames(names, constants2))
|
|
608
608
|
return;
|
|
609
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
609
|
+
this.iteration = optimizeExpr(this.iteration, names, constants2);
|
|
610
610
|
return this;
|
|
611
611
|
}
|
|
612
612
|
get names() {
|
|
@@ -642,10 +642,10 @@ var require_codegen = __commonJS({
|
|
|
642
642
|
render(opts) {
|
|
643
643
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
644
644
|
}
|
|
645
|
-
optimizeNames(names,
|
|
646
|
-
if (!super.optimizeNames(names,
|
|
645
|
+
optimizeNames(names, constants2) {
|
|
646
|
+
if (!super.optimizeNames(names, constants2))
|
|
647
647
|
return;
|
|
648
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
648
|
+
this.iterable = optimizeExpr(this.iterable, names, constants2);
|
|
649
649
|
return this;
|
|
650
650
|
}
|
|
651
651
|
get names() {
|
|
@@ -687,11 +687,11 @@ var require_codegen = __commonJS({
|
|
|
687
687
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
688
688
|
return this;
|
|
689
689
|
}
|
|
690
|
-
optimizeNames(names,
|
|
690
|
+
optimizeNames(names, constants2) {
|
|
691
691
|
var _a, _b;
|
|
692
|
-
super.optimizeNames(names,
|
|
693
|
-
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
694
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
692
|
+
super.optimizeNames(names, constants2);
|
|
693
|
+
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
|
|
694
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants2);
|
|
695
695
|
return this;
|
|
696
696
|
}
|
|
697
697
|
get names() {
|
|
@@ -992,7 +992,7 @@ var require_codegen = __commonJS({
|
|
|
992
992
|
function addExprNames(names, from) {
|
|
993
993
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
994
994
|
}
|
|
995
|
-
function optimizeExpr(expr, names,
|
|
995
|
+
function optimizeExpr(expr, names, constants2) {
|
|
996
996
|
if (expr instanceof code_1.Name)
|
|
997
997
|
return replaceName(expr);
|
|
998
998
|
if (!canOptimize(expr))
|
|
@@ -1007,14 +1007,14 @@ var require_codegen = __commonJS({
|
|
|
1007
1007
|
return items;
|
|
1008
1008
|
}, []));
|
|
1009
1009
|
function replaceName(n) {
|
|
1010
|
-
const c =
|
|
1010
|
+
const c = constants2[n.str];
|
|
1011
1011
|
if (c === void 0 || names[n.str] !== 1)
|
|
1012
1012
|
return n;
|
|
1013
1013
|
delete names[n.str];
|
|
1014
1014
|
return c;
|
|
1015
1015
|
}
|
|
1016
1016
|
function canOptimize(e) {
|
|
1017
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1017
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== void 0);
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
1020
1020
|
function subtractNames(names, from) {
|
|
@@ -2976,7 +2976,7 @@ var require_compile = __commonJS({
|
|
|
2976
2976
|
const schOrFunc = root.refs[ref];
|
|
2977
2977
|
if (schOrFunc)
|
|
2978
2978
|
return schOrFunc;
|
|
2979
|
-
let _sch =
|
|
2979
|
+
let _sch = resolve5.call(this, root, ref);
|
|
2980
2980
|
if (_sch === void 0) {
|
|
2981
2981
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2982
2982
|
const { schemaId } = this.opts;
|
|
@@ -3003,7 +3003,7 @@ var require_compile = __commonJS({
|
|
|
3003
3003
|
function sameSchemaEnv(s1, s2) {
|
|
3004
3004
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3005
3005
|
}
|
|
3006
|
-
function
|
|
3006
|
+
function resolve5(root, ref) {
|
|
3007
3007
|
let sch;
|
|
3008
3008
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3009
3009
|
ref = sch;
|
|
@@ -3578,7 +3578,7 @@ var require_fast_uri = __commonJS({
|
|
|
3578
3578
|
}
|
|
3579
3579
|
return uri;
|
|
3580
3580
|
}
|
|
3581
|
-
function
|
|
3581
|
+
function resolve5(baseURI, relativeURI, options) {
|
|
3582
3582
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3583
3583
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3584
3584
|
schemelessOptions.skipEscape = true;
|
|
@@ -3805,7 +3805,7 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3805
|
var fastUri = {
|
|
3806
3806
|
SCHEMES,
|
|
3807
3807
|
normalize,
|
|
3808
|
-
resolve:
|
|
3808
|
+
resolve: resolve5,
|
|
3809
3809
|
resolveComponent,
|
|
3810
3810
|
equal,
|
|
3811
3811
|
serialize,
|
|
@@ -8495,6 +8495,11 @@ var dev_spec_v5_schema_default = {
|
|
|
8495
8495
|
additionalProperties: false,
|
|
8496
8496
|
properties: {
|
|
8497
8497
|
id: { type: "string" },
|
|
8498
|
+
category: {
|
|
8499
|
+
type: "string",
|
|
8500
|
+
enum: ["HP", "EP", "BC", "NI", "IT"],
|
|
8501
|
+
description: "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
|
|
8502
|
+
},
|
|
8498
8503
|
given: { type: "string" },
|
|
8499
8504
|
when: { type: "string" },
|
|
8500
8505
|
then: { type: "string" },
|
|
@@ -8680,6 +8685,21 @@ var dev_spec_v5_schema_default = {
|
|
|
8680
8685
|
reason: { type: "string" }
|
|
8681
8686
|
}
|
|
8682
8687
|
}
|
|
8688
|
+
},
|
|
8689
|
+
implications: {
|
|
8690
|
+
type: "array",
|
|
8691
|
+
description: "Downstream behaviors/constraints this decision creates. Populated post-decision by orchestrator or L2.5 derivation step.",
|
|
8692
|
+
items: {
|
|
8693
|
+
type: "object",
|
|
8694
|
+
required: ["implication", "type"],
|
|
8695
|
+
additionalProperties: false,
|
|
8696
|
+
properties: {
|
|
8697
|
+
implication: { type: "string", description: "Concrete downstream behavior or constraint" },
|
|
8698
|
+
type: { type: "string", enum: ["deterministic", "context-dependent", "intent-dependent"], description: "deterministic: always true given decision. context-dependent: true given decision + project context. intent-dependent: depends on user preference (requires confirmation)." },
|
|
8699
|
+
status: { type: "string", enum: ["confirmed", "pending", "rejected"], default: "pending", description: "confirmed: user verified. pending: agent-derived, awaiting confirmation. rejected: user overrode." },
|
|
8700
|
+
conditional_on: { type: "string", description: "Other decision ID this implication depends on (e.g., 'D2'). Omit if unconditional." }
|
|
8701
|
+
}
|
|
8702
|
+
}
|
|
8683
8703
|
}
|
|
8684
8704
|
}
|
|
8685
8705
|
}
|
|
@@ -9217,6 +9237,7 @@ Usage:
|
|
|
9217
9237
|
hoyeon-cli spec status <path> Show task completion status (exit 0=done, 1=incomplete)
|
|
9218
9238
|
hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
|
|
9219
9239
|
hoyeon-cli spec check <path> Check internal consistency
|
|
9240
|
+
hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json] Check spec coverage (source.ref, decision coverage, scenario min count, orphan scenarios)
|
|
9220
9241
|
hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
|
|
9221
9242
|
hoyeon-cli spec guide [section] Show schema guide for a section
|
|
9222
9243
|
hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
|
|
@@ -10001,6 +10022,142 @@ async function handleMeta(args) {
|
|
|
10001
10022
|
process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
|
|
10002
10023
|
process.exit(0);
|
|
10003
10024
|
}
|
|
10025
|
+
function collectScenarioSets(specData) {
|
|
10026
|
+
const allScenarioIds = /* @__PURE__ */ new Set();
|
|
10027
|
+
for (const req of specData.requirements || []) {
|
|
10028
|
+
for (const sc of req.scenarios || []) {
|
|
10029
|
+
if (sc.id) allScenarioIds.add(sc.id);
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
const referencedScenarioIds = /* @__PURE__ */ new Set();
|
|
10033
|
+
for (const task of specData.tasks || []) {
|
|
10034
|
+
for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
|
|
10035
|
+
if (scenarioRef) referencedScenarioIds.add(scenarioRef);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
return { allScenarioIds, referencedScenarioIds };
|
|
10039
|
+
}
|
|
10040
|
+
var VALID_COVERAGE_LAYERS = ["decisions", "requirements", "scenarios", "tasks"];
|
|
10041
|
+
async function handleCoverage(args) {
|
|
10042
|
+
const parsed = parseArgs(args);
|
|
10043
|
+
const filePath = parsed._[0];
|
|
10044
|
+
if (!filePath) {
|
|
10045
|
+
process.stderr.write("Error: missing <path> argument\n");
|
|
10046
|
+
process.stderr.write("Usage: hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json]\n");
|
|
10047
|
+
process.exit(1);
|
|
10048
|
+
}
|
|
10049
|
+
const layer = parsed.layer;
|
|
10050
|
+
if (layer !== void 0 && !VALID_COVERAGE_LAYERS.includes(layer)) {
|
|
10051
|
+
process.stderr.write(`Error: invalid --layer '${layer}'. Valid values: ${VALID_COVERAGE_LAYERS.join(", ")}
|
|
10052
|
+
`);
|
|
10053
|
+
process.exit(1);
|
|
10054
|
+
}
|
|
10055
|
+
const useJson = parsed.json === true;
|
|
10056
|
+
const specData = loadSpec(resolve(filePath));
|
|
10057
|
+
const gaps = [];
|
|
10058
|
+
const decisions = specData.context?.decisions || specData.decisions || [];
|
|
10059
|
+
const requirements = specData.requirements || [];
|
|
10060
|
+
const decisionIds = new Set(decisions.map((d) => d.id).filter(Boolean));
|
|
10061
|
+
const runDecisions = !layer || layer === "decisions";
|
|
10062
|
+
const runRequirements = !layer || layer === "requirements";
|
|
10063
|
+
const runScenarios = !layer || layer === "scenarios";
|
|
10064
|
+
const runTasks = !layer || layer === "tasks";
|
|
10065
|
+
if (runDecisions && decisionIds.size > 0) {
|
|
10066
|
+
for (const req of requirements) {
|
|
10067
|
+
const ref = req.source?.ref;
|
|
10068
|
+
if (ref === void 0 || ref === null) {
|
|
10069
|
+
gaps.push({
|
|
10070
|
+
layer: "decisions",
|
|
10071
|
+
check: "source.ref-integrity",
|
|
10072
|
+
message: `requirement '${req.id}' has no source.ref (decisions exist \u2014 link required)`
|
|
10073
|
+
});
|
|
10074
|
+
} else if (!decisionIds.has(ref)) {
|
|
10075
|
+
gaps.push({
|
|
10076
|
+
layer: "decisions",
|
|
10077
|
+
check: "source.ref-integrity",
|
|
10078
|
+
message: `requirement '${req.id}' source.ref '${ref}' does not match any decision ID`
|
|
10079
|
+
});
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
if (runDecisions && decisionIds.size > 0 && requirements.length > 0) {
|
|
10084
|
+
const coveredDecisionIds = /* @__PURE__ */ new Set();
|
|
10085
|
+
for (const req of requirements) {
|
|
10086
|
+
const ref = req.source?.ref;
|
|
10087
|
+
if (ref) coveredDecisionIds.add(ref);
|
|
10088
|
+
}
|
|
10089
|
+
for (const decId of decisionIds) {
|
|
10090
|
+
if (!coveredDecisionIds.has(decId)) {
|
|
10091
|
+
gaps.push({
|
|
10092
|
+
layer: "decisions",
|
|
10093
|
+
check: "decision-coverage",
|
|
10094
|
+
message: `decision '${decId}' is not referenced by any requirement source.ref`
|
|
10095
|
+
});
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
10098
|
+
}
|
|
10099
|
+
if (runRequirements) {
|
|
10100
|
+
for (const req of requirements) {
|
|
10101
|
+
const scenarios = req.scenarios || [];
|
|
10102
|
+
const anyHasCategory = scenarios.some((sc) => sc.category !== void 0);
|
|
10103
|
+
if (anyHasCategory) {
|
|
10104
|
+
const categories = new Set(scenarios.map((sc) => sc.category).filter(Boolean));
|
|
10105
|
+
const missing = [];
|
|
10106
|
+
if (!categories.has("HP")) missing.push("HP");
|
|
10107
|
+
if (!categories.has("EP")) missing.push("EP");
|
|
10108
|
+
if (!categories.has("BC")) missing.push("BC");
|
|
10109
|
+
if (missing.length > 0) {
|
|
10110
|
+
gaps.push({
|
|
10111
|
+
layer: "requirements",
|
|
10112
|
+
check: "scenario-min-count",
|
|
10113
|
+
message: `requirement '${req.id}' is missing scenario categories: ${missing.join(", ")}`
|
|
10114
|
+
});
|
|
10115
|
+
}
|
|
10116
|
+
} else {
|
|
10117
|
+
if (scenarios.length < 3) {
|
|
10118
|
+
gaps.push({
|
|
10119
|
+
layer: "requirements",
|
|
10120
|
+
check: "scenario-min-count",
|
|
10121
|
+
message: `requirement '${req.id}' has ${scenarios.length} scenario(s) but needs at least 3 (count-only mode \u2014 no category field present)`
|
|
10122
|
+
});
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10126
|
+
}
|
|
10127
|
+
if (runScenarios && runTasks) {
|
|
10128
|
+
const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
|
|
10129
|
+
const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
|
|
10130
|
+
if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
|
|
10131
|
+
for (const scenarioId of allScenarioIds) {
|
|
10132
|
+
if (!referencedScenarioIds.has(scenarioId)) {
|
|
10133
|
+
gaps.push({
|
|
10134
|
+
layer: "scenarios",
|
|
10135
|
+
check: "orphan-scenario",
|
|
10136
|
+
message: `scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`
|
|
10137
|
+
});
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
10142
|
+
if (useJson) {
|
|
10143
|
+
const result = {
|
|
10144
|
+
coverage: gaps.length === 0 ? "pass" : "fail",
|
|
10145
|
+
gaps
|
|
10146
|
+
};
|
|
10147
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
10148
|
+
process.exit(gaps.length === 0 ? 0 : 1);
|
|
10149
|
+
}
|
|
10150
|
+
if (gaps.length > 0) {
|
|
10151
|
+
process.stderr.write("Coverage gaps found:\n");
|
|
10152
|
+
for (const gap of gaps) {
|
|
10153
|
+
process.stderr.write(` [${gap.layer}/${gap.check}] ${gap.message}
|
|
10154
|
+
`);
|
|
10155
|
+
}
|
|
10156
|
+
process.exit(1);
|
|
10157
|
+
}
|
|
10158
|
+
process.stdout.write("Coverage passed: all coverage checks OK\n");
|
|
10159
|
+
process.exit(0);
|
|
10160
|
+
}
|
|
10004
10161
|
async function handleCheck(args) {
|
|
10005
10162
|
const filePath = args[0];
|
|
10006
10163
|
if (!filePath) {
|
|
@@ -10045,12 +10202,7 @@ async function handleCheck(args) {
|
|
|
10045
10202
|
}
|
|
10046
10203
|
}
|
|
10047
10204
|
}
|
|
10048
|
-
const allScenarioIds
|
|
10049
|
-
for (const req of specData.requirements || []) {
|
|
10050
|
-
for (const sc of req.scenarios || []) {
|
|
10051
|
-
if (sc.id) allScenarioIds.add(sc.id);
|
|
10052
|
-
}
|
|
10053
|
-
}
|
|
10205
|
+
const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
|
|
10054
10206
|
for (const task of specData.tasks) {
|
|
10055
10207
|
for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
|
|
10056
10208
|
if (!allScenarioIds.has(scenarioRef)) {
|
|
@@ -10058,7 +10210,36 @@ async function handleCheck(args) {
|
|
|
10058
10210
|
}
|
|
10059
10211
|
}
|
|
10060
10212
|
}
|
|
10213
|
+
const decisionIds = new Set((specData.context?.decisions || specData.decisions || []).map((d) => d.id).filter(Boolean));
|
|
10214
|
+
for (const req of specData.requirements || []) {
|
|
10215
|
+
const ref = req.source?.ref;
|
|
10216
|
+
if (ref !== void 0 && ref !== null) {
|
|
10217
|
+
if (!decisionIds.has(ref)) {
|
|
10218
|
+
issues.push(`requirement '${req.id}' source.ref '${ref}' does not match any decision ID`);
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
}
|
|
10222
|
+
const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
|
|
10223
|
+
if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
|
|
10224
|
+
for (const scenarioId of allScenarioIds) {
|
|
10225
|
+
if (!referencedScenarioIds.has(scenarioId)) {
|
|
10226
|
+
issues.push(`scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`);
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10061
10230
|
const warnings = [];
|
|
10231
|
+
if ((specData.context?.decisions || specData.decisions || []).length > 0 && (specData.requirements || []).length > 0) {
|
|
10232
|
+
const coveredDecisionIds = /* @__PURE__ */ new Set();
|
|
10233
|
+
for (const req of specData.requirements || []) {
|
|
10234
|
+
const ref = req.source?.ref;
|
|
10235
|
+
if (ref) coveredDecisionIds.add(ref);
|
|
10236
|
+
}
|
|
10237
|
+
for (const decId of decisionIds) {
|
|
10238
|
+
if (!coveredDecisionIds.has(decId)) {
|
|
10239
|
+
warnings.push(`decision '${decId}' is not referenced by any requirement source.ref`);
|
|
10240
|
+
}
|
|
10241
|
+
}
|
|
10242
|
+
}
|
|
10062
10243
|
const fileScopeMap = /* @__PURE__ */ new Map();
|
|
10063
10244
|
for (const task of specData.tasks) {
|
|
10064
10245
|
for (const file of task.file_scope || []) {
|
|
@@ -10818,6 +10999,8 @@ async function spec(args) {
|
|
|
10818
10999
|
await handleStatus(args.slice(1));
|
|
10819
11000
|
} else if (subcommand === "meta") {
|
|
10820
11001
|
await handleMeta(args.slice(1));
|
|
11002
|
+
} else if (subcommand === "coverage") {
|
|
11003
|
+
await handleCoverage(args.slice(1));
|
|
10821
11004
|
} else if (subcommand === "check") {
|
|
10822
11005
|
await handleCheck(args.slice(1));
|
|
10823
11006
|
} else if (subcommand === "amend") {
|
|
@@ -11613,6 +11796,197 @@ async function feedback(args) {
|
|
|
11613
11796
|
}
|
|
11614
11797
|
}
|
|
11615
11798
|
|
|
11799
|
+
// src/handlers/settings-validate.js
|
|
11800
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2, accessSync, constants } from "fs";
|
|
11801
|
+
import { resolve as resolve4, dirname as dirname4, join as join2 } from "path";
|
|
11802
|
+
var SETTINGS_HELP = `
|
|
11803
|
+
Usage:
|
|
11804
|
+
hoyeon-cli settings validate Validate .claude/settings.json hook configuration
|
|
11805
|
+
|
|
11806
|
+
Options:
|
|
11807
|
+
--help, -h Show this help message
|
|
11808
|
+
|
|
11809
|
+
Examples:
|
|
11810
|
+
hoyeon-cli settings validate
|
|
11811
|
+
`;
|
|
11812
|
+
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
11813
|
+
"SessionStart",
|
|
11814
|
+
"SessionEnd",
|
|
11815
|
+
"UserPromptSubmit",
|
|
11816
|
+
"PreToolUse",
|
|
11817
|
+
"PostToolUse",
|
|
11818
|
+
"PostToolUseFailure",
|
|
11819
|
+
"Stop"
|
|
11820
|
+
]);
|
|
11821
|
+
function findSettingsJson(startDir) {
|
|
11822
|
+
let dir = startDir;
|
|
11823
|
+
while (true) {
|
|
11824
|
+
const candidate = join2(dir, ".claude", "settings.json");
|
|
11825
|
+
if (existsSync2(candidate)) {
|
|
11826
|
+
return candidate;
|
|
11827
|
+
}
|
|
11828
|
+
const parent = dirname4(dir);
|
|
11829
|
+
if (parent === dir) break;
|
|
11830
|
+
dir = parent;
|
|
11831
|
+
}
|
|
11832
|
+
return null;
|
|
11833
|
+
}
|
|
11834
|
+
function collectHookCommands(hooks) {
|
|
11835
|
+
const entries = [];
|
|
11836
|
+
for (const [eventType, matchers] of Object.entries(hooks)) {
|
|
11837
|
+
if (!Array.isArray(matchers)) continue;
|
|
11838
|
+
for (const matcherObj of matchers) {
|
|
11839
|
+
const matcher = matcherObj.matcher ?? "";
|
|
11840
|
+
const hookList = matcherObj.hooks ?? [];
|
|
11841
|
+
for (const hook of hookList) {
|
|
11842
|
+
if (hook.type === "command" && typeof hook.command === "string") {
|
|
11843
|
+
entries.push({ eventType, matcher, command: hook.command });
|
|
11844
|
+
}
|
|
11845
|
+
}
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
return entries;
|
|
11849
|
+
}
|
|
11850
|
+
async function handleValidate2() {
|
|
11851
|
+
const startDir = process.cwd();
|
|
11852
|
+
const settingsPath = findSettingsJson(startDir);
|
|
11853
|
+
if (!settingsPath) {
|
|
11854
|
+
process.stderr.write("Error: .claude/settings.json not found (searched from cwd upward)\n");
|
|
11855
|
+
process.exit(1);
|
|
11856
|
+
}
|
|
11857
|
+
const projectRoot = dirname4(dirname4(settingsPath));
|
|
11858
|
+
let settings2;
|
|
11859
|
+
try {
|
|
11860
|
+
settings2 = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
11861
|
+
} catch (err) {
|
|
11862
|
+
process.stderr.write(`Error: failed to parse settings.json: ${err.message}
|
|
11863
|
+
`);
|
|
11864
|
+
process.exit(1);
|
|
11865
|
+
}
|
|
11866
|
+
const hooks = settings2.hooks ?? {};
|
|
11867
|
+
const hookEntries = collectHookCommands(hooks);
|
|
11868
|
+
process.stdout.write(`Settings: ${settingsPath}
|
|
11869
|
+
`);
|
|
11870
|
+
process.stdout.write(`Project root: ${projectRoot}
|
|
11871
|
+
|
|
11872
|
+
`);
|
|
11873
|
+
let hasFailure = false;
|
|
11874
|
+
process.stdout.write("Check 1: Hook script path existence\n");
|
|
11875
|
+
const missingPaths = [];
|
|
11876
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11877
|
+
const absPath = resolve4(projectRoot, command);
|
|
11878
|
+
if (!existsSync2(absPath)) {
|
|
11879
|
+
missingPaths.push({ eventType, matcher, command, absPath });
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11882
|
+
if (missingPaths.length === 0) {
|
|
11883
|
+
process.stdout.write(" PASS: All hook script paths exist\n");
|
|
11884
|
+
} else {
|
|
11885
|
+
hasFailure = true;
|
|
11886
|
+
process.stdout.write(` FAIL: ${missingPaths.length} missing path(s)
|
|
11887
|
+
`);
|
|
11888
|
+
for (const { eventType, command, absPath } of missingPaths) {
|
|
11889
|
+
process.stdout.write(` [${eventType}] ${command}
|
|
11890
|
+
`);
|
|
11891
|
+
process.stdout.write(` -> ${absPath} (not found)
|
|
11892
|
+
`);
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
process.stdout.write("\nCheck 2: Hook script executable bit\n");
|
|
11896
|
+
const nonExecutable = [];
|
|
11897
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11898
|
+
const absPath = resolve4(projectRoot, command);
|
|
11899
|
+
if (!existsSync2(absPath)) continue;
|
|
11900
|
+
try {
|
|
11901
|
+
accessSync(absPath, constants.X_OK);
|
|
11902
|
+
} catch {
|
|
11903
|
+
nonExecutable.push({ eventType, command, absPath });
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
if (nonExecutable.length === 0) {
|
|
11907
|
+
process.stdout.write(" PASS: All hook scripts are executable\n");
|
|
11908
|
+
} else {
|
|
11909
|
+
hasFailure = true;
|
|
11910
|
+
process.stdout.write(` FAIL: ${nonExecutable.length} non-executable script(s)
|
|
11911
|
+
`);
|
|
11912
|
+
for (const { eventType, command } of nonExecutable) {
|
|
11913
|
+
process.stdout.write(` [${eventType}] ${command} (not executable)
|
|
11914
|
+
`);
|
|
11915
|
+
}
|
|
11916
|
+
}
|
|
11917
|
+
process.stdout.write("\nCheck 3: Valid event types\n");
|
|
11918
|
+
const invalidEventTypes = [];
|
|
11919
|
+
for (const eventType of Object.keys(hooks)) {
|
|
11920
|
+
if (!VALID_EVENT_TYPES.has(eventType)) {
|
|
11921
|
+
invalidEventTypes.push(eventType);
|
|
11922
|
+
}
|
|
11923
|
+
}
|
|
11924
|
+
if (invalidEventTypes.length === 0) {
|
|
11925
|
+
process.stdout.write(" PASS: All event types are valid\n");
|
|
11926
|
+
} else {
|
|
11927
|
+
hasFailure = true;
|
|
11928
|
+
process.stdout.write(` FAIL: ${invalidEventTypes.length} invalid event type(s)
|
|
11929
|
+
`);
|
|
11930
|
+
for (const et of invalidEventTypes) {
|
|
11931
|
+
process.stdout.write(` '${et}' is not a valid event type
|
|
11932
|
+
`);
|
|
11933
|
+
}
|
|
11934
|
+
process.stdout.write(` Valid event types: ${[...VALID_EVENT_TYPES].join(", ")}
|
|
11935
|
+
`);
|
|
11936
|
+
}
|
|
11937
|
+
process.stdout.write("\nCheck 4: Duplicate scripts in same event+matcher\n");
|
|
11938
|
+
const seen = /* @__PURE__ */ new Map();
|
|
11939
|
+
const duplicates = [];
|
|
11940
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11941
|
+
const key = `${eventType}::${matcher}`;
|
|
11942
|
+
if (!seen.has(key)) {
|
|
11943
|
+
seen.set(key, /* @__PURE__ */ new Set());
|
|
11944
|
+
}
|
|
11945
|
+
const commandSet = seen.get(key);
|
|
11946
|
+
if (commandSet.has(command)) {
|
|
11947
|
+
duplicates.push({ eventType, matcher, command });
|
|
11948
|
+
} else {
|
|
11949
|
+
commandSet.add(command);
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
11952
|
+
if (duplicates.length === 0) {
|
|
11953
|
+
process.stdout.write(" PASS: No duplicate scripts in same event+matcher\n");
|
|
11954
|
+
} else {
|
|
11955
|
+
hasFailure = true;
|
|
11956
|
+
process.stdout.write(` FAIL: ${duplicates.length} duplicate(s) found
|
|
11957
|
+
`);
|
|
11958
|
+
for (const { eventType, matcher, command } of duplicates) {
|
|
11959
|
+
const matcherLabel = matcher ? `matcher="${matcher}"` : 'matcher=""';
|
|
11960
|
+
process.stdout.write(` [${eventType}][${matcherLabel}] ${command}
|
|
11961
|
+
`);
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
process.stdout.write("\n");
|
|
11965
|
+
if (hasFailure) {
|
|
11966
|
+
process.stdout.write("Result: FAIL\n");
|
|
11967
|
+
process.exit(1);
|
|
11968
|
+
} else {
|
|
11969
|
+
process.stdout.write("Result: PASS\n");
|
|
11970
|
+
process.exit(0);
|
|
11971
|
+
}
|
|
11972
|
+
}
|
|
11973
|
+
async function settings(args) {
|
|
11974
|
+
const subcommand = args[0];
|
|
11975
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
11976
|
+
process.stdout.write(SETTINGS_HELP);
|
|
11977
|
+
process.exit(0);
|
|
11978
|
+
}
|
|
11979
|
+
if (subcommand === "validate") {
|
|
11980
|
+
await handleValidate2();
|
|
11981
|
+
} else {
|
|
11982
|
+
process.stderr.write(`Error: unknown settings subcommand '${subcommand}'
|
|
11983
|
+
`);
|
|
11984
|
+
process.stderr.write(`Run 'hoyeon-cli settings --help' for usage.
|
|
11985
|
+
`);
|
|
11986
|
+
process.exit(1);
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
|
|
11616
11990
|
// bin/dev-cli.js
|
|
11617
11991
|
var USAGE = `
|
|
11618
11992
|
hoyeon-cli \u2014 Developer workflow CLI
|
|
@@ -11639,7 +12013,8 @@ var SUBCOMMANDS = {
|
|
|
11639
12013
|
spec,
|
|
11640
12014
|
state,
|
|
11641
12015
|
session,
|
|
11642
|
-
feedback
|
|
12016
|
+
feedback,
|
|
12017
|
+
settings
|
|
11643
12018
|
};
|
|
11644
12019
|
async function main() {
|
|
11645
12020
|
const args = process.argv.slice(2);
|
|
@@ -11648,7 +12023,7 @@ async function main() {
|
|
|
11648
12023
|
process.exit(0);
|
|
11649
12024
|
}
|
|
11650
12025
|
if (args[0] === "--version") {
|
|
11651
|
-
const version = true ? "1.
|
|
12026
|
+
const version = true ? "1.1.1" : "dev";
|
|
11652
12027
|
process.stdout.write(`hoyeon-cli v${version}
|
|
11653
12028
|
`);
|
|
11654
12029
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-attention/hoyeon-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Developer CLI for managing dev workflow state",
|
|
6
6
|
"bin": {
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "node build.mjs"
|
|
19
|
-
},
|
|
20
17
|
"dependencies": {
|
|
21
18
|
"ajv": "^8.0.0",
|
|
22
19
|
"ajv-formats": "^3.0.0"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"esbuild": "^0.27.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "node build.mjs",
|
|
26
|
+
"test": "node --test tests/*.test.mjs"
|
|
26
27
|
}
|
|
27
|
-
}
|
|
28
|
+
}
|
|
@@ -116,6 +116,11 @@
|
|
|
116
116
|
"additionalProperties": false,
|
|
117
117
|
"properties": {
|
|
118
118
|
"id": { "type": "string" },
|
|
119
|
+
"category": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"enum": ["HP", "EP", "BC", "NI", "IT"],
|
|
122
|
+
"description": "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
|
|
123
|
+
},
|
|
119
124
|
"given": { "type": "string" },
|
|
120
125
|
"when": { "type": "string" },
|
|
121
126
|
"then": { "type": "string" },
|
|
@@ -301,6 +306,21 @@
|
|
|
301
306
|
"reason": { "type": "string" }
|
|
302
307
|
}
|
|
303
308
|
}
|
|
309
|
+
},
|
|
310
|
+
"implications": {
|
|
311
|
+
"type": "array",
|
|
312
|
+
"description": "Downstream behaviors/constraints this decision creates. Populated post-decision by orchestrator or L2.5 derivation step.",
|
|
313
|
+
"items": {
|
|
314
|
+
"type": "object",
|
|
315
|
+
"required": ["implication", "type"],
|
|
316
|
+
"additionalProperties": false,
|
|
317
|
+
"properties": {
|
|
318
|
+
"implication": { "type": "string", "description": "Concrete downstream behavior or constraint" },
|
|
319
|
+
"type": { "type": "string", "enum": ["deterministic", "context-dependent", "intent-dependent"], "description": "deterministic: always true given decision. context-dependent: true given decision + project context. intent-dependent: depends on user preference (requires confirmation)." },
|
|
320
|
+
"status": { "type": "string", "enum": ["confirmed", "pending", "rejected"], "default": "pending", "description": "confirmed: user verified. pending: agent-derived, awaiting confirmation. rejected: user overrode." },
|
|
321
|
+
"conditional_on": { "type": "string", "description": "Other decision ID this implication depends on (e.g., 'D2'). Omit if unconditional." }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
304
324
|
}
|
|
305
325
|
}
|
|
306
326
|
}
|