@llm4ts/shell 2.47.1 → 2.48.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.
@@ -83,6 +83,7 @@ export const mvpPlanInstructions = (epicId, guidance, orientation) => [
83
83
  " `acceptance` outcome, and none depends on a parked one.",
84
84
  "- Prefer fewer, thinner stories: the thinnest end-to-end path that works.",
85
85
  "- Items an approved brief marks deferred or dropped are parked, never planned.",
86
+ "- Leave `testsDeferred` out: every MVP story is written test-first.",
86
87
  'Add `"later":[…]` to each story and `"parked":[…]` beside `"stories"` in the JSON.'
87
88
  ].join("\n");
88
89
  /** The MVP planner: writes `mvp-cut.json` beside the plan with the stories parked whole. */
@@ -750,7 +750,8 @@ export const storyJsonSchema = {
750
750
  readFirst: { type: "array", items: { type: "string" } },
751
751
  acceptance: { type: "array", items: { type: "string" } },
752
752
  provides: { type: "array", items: { type: "string" } },
753
- testsChange: { type: "boolean" }
753
+ testsChange: { type: "boolean" },
754
+ testsDeferred: { type: "string" }
754
755
  },
755
756
  required: [
756
757
  "id",
@@ -797,6 +798,10 @@ orientation) => [
797
798
  "- `testsChange`: true only for a story whose job includes deleting, skipping or rewriting",
798
799
  " existing tests (a migration of a test suite, a removed feature). Every other story leaves",
799
800
  " it out: the gates fail a story that deletes a test file or adds a skip marker.",
801
+ "- `testsDeferred`: only for a story whose code runs nowhere but a live runtime the repository",
802
+ " cannot host in a test (IBM ACE message flows and ESQL, a mainframe transaction): one",
803
+ " sentence naming that runtime. The judge then does not ask it for tests. Never set it because",
804
+ " tests are hard to write; every other story leaves it out and owns its test files.",
800
805
  "- `owned` sets are pairwise DISJOINT: no path prefix appears under two stories.",
801
806
  "- Shared surfaces (the kit, the theme, house rules) are never edited by a feature story. A new",
802
807
  " shared component is its own story, and every story using it depends on it.",
@@ -998,10 +1003,15 @@ export const storyDimensions = [
998
1003
  * a partial score elsewhere is a Warning a follow-up round can close.
999
1004
  */
1000
1005
  export const judgeSeverity = (name, score) => name === "provides" || score <= 0 ? "Critical" : "Warning";
1006
+ /** The dimensions a story is held to: all of them, less `tests` when the plan deferred them. */
1007
+ export const judgedDimensionsOf = (story) => storyDimensions
1008
+ .map((dimension) => dimension.name)
1009
+ .filter((name) => story.testsDeferred === undefined || name !== "tests");
1001
1010
  const subBar = (scored, story) => ReviewResult.make({
1002
1011
  issues: scored.scores
1003
- .filter((score) => score.score <
1004
- (storyDimensions.find((dimension) => dimension.name === score.name)?.maxScore ?? 2))
1012
+ .filter((score) => judgedDimensionsOf(story).includes(score.name) &&
1013
+ score.score <
1014
+ (storyDimensions.find((dimension) => dimension.name === score.name)?.maxScore ?? 2))
1005
1015
  .map((score) => ReviewIssue.make({
1006
1016
  severity: judgeSeverity(score.name, score.score),
1007
1017
  title: `judge[${story.id}]: ${score.name} scored ${score.score}`,
@@ -1009,6 +1019,13 @@ const subBar = (scored, story) => ReviewResult.make({
1009
1019
  })),
1010
1020
  summary: `judge:${story.id}`
1011
1021
  });
1022
+ /** What a judge is told about a story the plan exempted from tests; nothing otherwise. */
1023
+ export const testsDeferredLines = (reason) => reason === undefined
1024
+ ? []
1025
+ : [
1026
+ `Tests deferred by the plan: ${reason}`,
1027
+ "No test file is expected; do not mark any dimension down for its absence."
1028
+ ];
1012
1029
  /**
1013
1030
  * What the story judge reads beside the diff: the story, and what every
1014
1031
  * story it depends on provides. Without the dependencies the judge cannot
@@ -1039,6 +1056,7 @@ export const storyJudgeQuery = (story, plan, subject = "diff") => {
1039
1056
  `Provides: ${story.provides.join(", ") || "(none)"}`,
1040
1057
  `Owned paths: ${story.owned.join(", ")}`,
1041
1058
  `Shared read-only: ${story.sharedReadOnly.join(", ") || "(none)"}`,
1059
+ ...testsDeferredLines(story.testsDeferred),
1042
1060
  ...(dependencies.length === 0
1043
1061
  ? []
1044
1062
  : [
@@ -17,10 +17,10 @@ import { answerKey, decideRule, decisionKey, publishBlackboardRun, runErrorToFlo
17
17
  import { cap } from "@llm4ts/flow/Context";
18
18
  import { FlowLlmError } from "@llm4ts/flow/FlowError";
19
19
  import { JudgmentObserved, publishJudgmentObserved } from "@llm4ts/flow/FlowEvents";
20
- import { certaintyOf, decide, judgmentOf } from "@llm4ts/flow/Judgment";
20
+ import { certaintyOf, Decision, decide, judgmentOf } from "@llm4ts/flow/Judgment";
21
21
  import { ReviewIssue, ReviewResult } from "@llm4ts/flow/Review";
22
22
  import { dependenciesOf } from "@llm4ts/flow/StoryPlan";
23
- import { storyDimensions } from "./epic-stories.js";
23
+ import { storyDimensions, testsDeferredLines } from "./epic-stories.js";
24
24
  export class StoryBrief extends Schema.Class("StoryBrief")({
25
25
  id: Schema.String,
26
26
  title: Schema.String,
@@ -29,7 +29,8 @@ export class StoryBrief extends Schema.Class("StoryBrief")({
29
29
  provides: Schema.Array(Schema.String),
30
30
  owned: Schema.Array(Schema.String),
31
31
  sharedReadOnly: Schema.Array(Schema.String),
32
- dependencies: Schema.Array(Schema.Struct({ id: Schema.String, provides: Schema.Array(Schema.String) }))
32
+ dependencies: Schema.Array(Schema.Struct({ id: Schema.String, provides: Schema.Array(Schema.String) })),
33
+ testsDeferred: Schema.optionalKey(Schema.String)
33
34
  }) {
34
35
  }
35
36
  export const storyBriefOf = (story, plan) => StoryBrief.make({
@@ -45,7 +46,8 @@ export const storyBriefOf = (story, plan) => StoryBrief.make({
45
46
  : dependenciesOf(plan, story.id).flatMap((id) => {
46
47
  const dependency = plan.story(id);
47
48
  return dependency === undefined ? [] : [{ id, provides: dependency.provides }];
48
- })
49
+ }),
50
+ ...(story.testsDeferred === undefined ? {} : { testsDeferred: story.testsDeferred })
49
51
  });
50
52
  // ---- Facts ------------------------------------------------------------------
51
53
  export const storyBrief = makeKey("story.brief", StoryBrief);
@@ -53,6 +55,7 @@ export const storyDiff = makeKey("story.diff", Schema.String);
53
55
  export const houseRules = makeKey("story.houseRules", Schema.String);
54
56
  export const mergeable = makeKey("story.mergeable", Schema.Boolean);
55
57
  export const issues = makeKey("story.issues", Schema.Array(ReviewIssue));
58
+ const decisions = makeKey("story.decisions", Schema.Array(Decision));
56
59
  /** One judged answer and one decision per story dimension, keyed by the dimension's name. */
57
60
  const judged = (name) => answerKey(`judge.${name}`);
58
61
  const decided = (name) => decisionKey(`decision.${name}`);
@@ -86,6 +89,7 @@ export const stateOf = (brief, diff, rules) => [
86
89
  `Provides: ${brief.provides.join(", ") || "(none)"}`,
87
90
  `Owned paths: ${brief.owned.join(", ")}`,
88
91
  `Shared read-only: ${brief.sharedReadOnly.join(", ") || "(none)"}`,
92
+ ...testsDeferredLines(brief.testsDeferred),
89
93
  ...(brief.dependencies.length === 0
90
94
  ? []
91
95
  : [
@@ -122,25 +126,34 @@ export const makeStoryBoard = (policy) => {
122
126
  return answer?.type === "score" ? [d.answer.of(answer)] : [];
123
127
  })
124
128
  });
125
- const decisions = dimensions.map((d) => decideRule({
129
+ const deciders = dimensions.map((d) => decideRule({
126
130
  name: `decide-${d.dimension.name}`,
127
131
  answer: d.answer,
128
132
  decision: d.decision,
129
133
  ...(policy === undefined ? {} : { policy })
130
134
  }));
135
+ // The four decisions as one fact, so the bar can also read the brief.
136
+ const joined = derive({
137
+ name: "decisions",
138
+ condition: all(provides.decision, scope.decision, houseStyle.decision, tests.decision),
139
+ produces: [decisions],
140
+ derive: (verdicts) => [decisions.of(verdicts)]
141
+ });
131
142
  const bar = derive({
132
143
  name: "bar",
133
- condition: all(provides.answer, scope.answer, houseStyle.answer, tests.answer, provides.decision, scope.decision, houseStyle.decision, tests.decision),
144
+ condition: all(storyBrief, provides.answer, scope.answer, houseStyle.answer, tests.answer, decisions),
134
145
  produces: [mergeable, issues],
135
- derive: ([a1, a2, a3, a4, v1, v2, v3, v4]) => {
146
+ derive: ([brief, a1, a2, a3, a4, verdicts]) => {
136
147
  const found = [];
137
148
  const answers = [a1, a2, a3, a4];
138
- const verdicts = [v1, v2, v3, v4];
139
149
  dimensions.forEach((d, index) => {
140
150
  const answer = answers[index];
141
151
  const verdict = verdicts[index];
142
152
  if (answer === undefined || verdict === undefined || answer.type !== "score")
143
153
  return;
154
+ // The plan deferred this story's tests (it runs only in a live runtime).
155
+ if (d === tests && brief.testsDeferred !== undefined)
156
+ return;
144
157
  if (answer.score < passingScore) {
145
158
  found.push(ReviewIssue.make({
146
159
  severity: "Critical",
@@ -163,7 +176,7 @@ export const makeStoryBoard = (policy) => {
163
176
  name: "story-board",
164
177
  imports: [storyBrief, storyDiff, houseRules],
165
178
  exports: [mergeable, issues],
166
- rules: [storyJudge, ...decisions, bar]
179
+ rules: [storyJudge, ...deciders, joined, bar]
167
180
  });
168
181
  };
169
182
  /**
@@ -2,7 +2,7 @@
2
2
 
3
3
  source: jsp
4
4
  scaffold: ../../scaffolds/nextjs-spa
5
- sources: .*\.(jsp|java|xml)
5
+ sources: .*\.(jsp|jspf|js|java|xml)$
6
6
  spec-schema: pagespec
7
7
  programs: .*\.jsp
8
8
  specs-dir: docs/modernization/specs
@@ -27,54 +27,131 @@ setup: pnpm install --offline
27
27
 
28
28
  ## Coverage: servlet-url
29
29
 
30
- files: .*web\.xml
30
+ files: .*web\.xml$
31
31
  unit: <url-pattern>([^<]+)</url-pattern>
32
32
 
33
33
  ## Coverage: jsp-form
34
34
 
35
- files: .*\.jsp
35
+ files: .*\.jsp$
36
36
  unit: action="([^"]+)"
37
37
 
38
38
  ## Coverage: jsp-ajax
39
39
 
40
- files: .*\.jsp
40
+ files: .*\.jsp$
41
41
  unit: url:\s*['"]([^'"]+)['"]
42
42
 
43
43
  ## Survey: jsp-include
44
44
 
45
- files: .*\.jsp
45
+ files: .*\.jsp$
46
46
  unit: <jsp:include page="([^"]+)"
47
47
 
48
+ ## Survey: jsp-static-include
49
+
50
+ files: .*\.jspf?$
51
+ unit: <%@\s*include\s+file="([^"]+)"
52
+
48
53
  ## Survey: servlet-class
49
54
 
50
- files: .*web\.xml
55
+ files: .*web\.xml$
51
56
  unit: <servlet-class>[a-z.]*\.([A-Za-z0-9]+)</servlet-class>
52
57
 
53
58
  ## Node: ajax-call
54
59
 
55
- files: .*\.(jsp|js)
60
+ files: .*\.(jsp|js)$
56
61
  pattern: (?:["']?url["']?\s*:\s*|\$\.(?:get|post|getJSON)\(\s*)['"](?<name>[^'"]+)['"]
57
62
  attrs: url=name
58
63
 
59
64
  ## Node: form
60
65
 
61
- files: .*\.jsp
66
+ files: .*\.jsp$
62
67
  pattern: <(?:[a-z]+:)?[Ff][Oo][Rr][Mm]\b[^>]*\b[Aa][Cc][Tt][Ii][Oo][Nn]=["'](?<name>[^"']+)["']
63
68
  attrs: url=name
64
69
 
65
70
  ## Node: servlet-mapping
66
71
 
67
- files: .*web\.xml
72
+ files: .*web\.xml$
68
73
  pattern: <servlet-mapping>\s*<servlet-name>(?<name>[^<]+)</servlet-name>\s*<url-pattern>(?<url>[^<]+)</url-pattern>
69
74
  descriptor: yes
70
75
 
71
76
  ## Node: servlet-decl
72
77
 
73
- files: .*web\.xml
78
+ files: .*web\.xml$
74
79
  pattern: <servlet>(?:(?!</servlet>)[\s\S])*?<servlet-name>(?<name>[^<]+)</servlet-name>(?:(?!</servlet>)[\s\S])*?<servlet-class>(?:[a-z0-9_]+\.)*(?<class>[A-Za-z0-9_]+)</servlet-class>
75
80
  descriptor: yes
76
81
  anchor: class
77
82
 
83
+ ## Node: rest-resource
84
+
85
+ files: .*\.java$
86
+ pattern: @(?:Path|RequestMapping)\(\s*(?:value\s*=\s*)?"(?<url>[^"]+)"\s*\)(?:\s*@\w+(?:\([^)]*\))?)*\s*public\s+(?:abstract\s+)?class\s+(?<name>\w+)
87
+
88
+ ## Node: java-bean
89
+
90
+ files: .*\.java$
91
+ pattern: @(?:Named|ManagedBean|Stateless|Stateful|Singleton|Component|Service|Controller)\b(?:\([^)]*\))?(?:\s*@\w+(?:\([^)]*\))?)*\s*public\s+class\s+(?<name>\w+)
92
+
93
+ ## Node: el-bean-ref
94
+
95
+ files: .*\.jspf?$
96
+ pattern: \$\{(?<name>[a-z]\w*)\.
97
+
98
+ ## Edge: jsp-usebean
99
+
100
+ files: .*\.jspf?$
101
+ pattern: <jsp:useBean\b[^>]*\bclass="(?:[\w]+\.)*(?<to>\w+)"
102
+ unknown: drop
103
+
104
+ ## Edge: jsp-static-call
105
+
106
+ files: .*\.jspf?$
107
+ pattern: \b(?<to>[A-Z][A-Za-z0-9]+)\.[a-z]\w*\(
108
+ unknown: drop
109
+
110
+ ## Edge: jsp-new
111
+
112
+ files: .*\.jspf?$
113
+ pattern: \bnew\s+(?<to>[A-Z]\w*)\s*\(
114
+ unknown: drop
115
+
116
+ ## Node: servlet-jsp
117
+
118
+ files: .*web\.xml$
119
+ pattern: <servlet>(?:(?!</servlet>)[\s\S])*?<servlet-name>(?<name>[^<]+)</servlet-name>(?:(?!</servlet>)[\s\S])*?<jsp-file>(?<page>[^<]+)</jsp-file>
120
+ descriptor: yes
121
+ anchor: page
122
+
123
+ ## Edge: jsp-form-to-page
124
+
125
+ files: .*\.jsp$
126
+ pattern: <(?:[a-z]+:)?[Ff][Oo][Rr][Mm]\b[^>]*\b[Aa][Cc][Tt][Ii][Oo][Nn]=["'](?<to>[^"'?]+\.jsp)
127
+ from: form
128
+ unknown: drop
129
+
130
+ ## Edge: jsp-ajax-to-page
131
+
132
+ files: .*\.(jsp|js)$
133
+ pattern: (?:["']?url["']?\s*:\s*|\$\.(?:get|post|getJSON)\(\s*)['"](?<to>[^'"?]+\.jsp)
134
+ from: ajax-call
135
+ unknown: drop
136
+
137
+ ## Edge: java-import
138
+
139
+ files: .*\.java$
140
+ pattern: ^import\s+(?:static\s+)?[\w.]*\.(?<to>[A-Z]\w*)\s*;
141
+ unknown: drop
142
+
143
+ ## Edge: java-new
144
+
145
+ files: .*\.java$
146
+ pattern: \bnew\s+(?<to>[A-Z]\w*)\s*[<(]
147
+ unknown: drop
148
+
149
+ ## Edge: java-static-call
150
+
151
+ files: .*\.java$
152
+ pattern: \b(?<to>[A-Z]\w+)\.[a-z]\w*\(
153
+ unknown: drop
154
+
78
155
  ## Join: jsp-ajax-target
79
156
 
80
157
  from: ajax-call.url
@@ -87,13 +164,31 @@ from: form.url
87
164
  to: servlet-mapping.url
88
165
  match: url
89
166
 
167
+ ## Join: jsp-ajax-rest
168
+
169
+ from: ajax-call.url
170
+ to: rest-resource.url
171
+ match: url-path
172
+
173
+ ## Join: jsp-el-bean
174
+
175
+ from: el-bean-ref.name
176
+ to: java-bean.name
177
+ match: exact-ci
178
+
90
179
  ## Join: servlet-wiring
91
180
 
92
181
  from: servlet-mapping.name
93
182
  to: servlet-decl.name
94
183
  scope: file
95
184
 
185
+ ## Join: servlet-wiring
186
+
187
+ from: servlet-mapping.name
188
+ to: servlet-jsp.name
189
+ scope: file
190
+
96
191
  ## Consolidate
97
192
 
98
- - cluster: jsp-form-action, jsp-ajax-target
99
- - context: jsp-include
193
+ - cluster: jsp-form-action, jsp-ajax-target, jsp-form-to-page, jsp-ajax-to-page, jsp-ajax-rest
194
+ - context: jsp-include, jsp-static-include
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "2.47.1",
3
+ "version": "2.48.0",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -61,9 +61,9 @@
61
61
  "dependencies": {
62
62
  "@effect/platform-node": "4.0.0",
63
63
  "@effect/platform-node-shared": "4.0.0",
64
- "@llm4ts/core": "2.47.1",
65
- "@llm4ts/flow": "2.47.1",
66
- "@llm4ts/runner": "2.47.1"
64
+ "@llm4ts/flow": "2.48.0",
65
+ "@llm4ts/core": "2.48.0",
66
+ "@llm4ts/runner": "2.48.0"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "effect": "4.0.0"