dsh-model-everything 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.en-US.md +332 -0
- package/README.md +332 -3
- package/cordis.patch.yml +20 -0
- package/examples/README.md +48 -0
- package/examples/control-dispatcher.json +113 -0
- package/examples/control-dispatcher.overbudget.json +113 -0
- package/examples/ecommerce-data-model.json +185 -0
- package/examples/order-state-machine.after.json +116 -0
- package/examples/order-state-machine.before.json +101 -0
- package/examples/user-migration.after.json +29 -0
- package/examples/user-migration.before.json +29 -0
- package/gate/locale/en.json +6 -0
- package/gate/locale/zh.json +6 -0
- package/gate/package.json +5 -0
- package/lib/baseline.js +249 -0
- package/lib/compose-tool.js +80 -0
- package/lib/concurrency-tool.js +40 -0
- package/lib/concurrency.js +206 -0
- package/lib/data-engine.js +1141 -0
- package/lib/data-tool.js +87 -0
- package/lib/design-engine.js +1149 -0
- package/lib/design-tool.js +63 -0
- package/lib/engine.js +4343 -0
- package/lib/export-tool.js +78 -0
- package/lib/exporters.js +434 -0
- package/lib/gate.js +274 -0
- package/lib/granularity.js +237 -0
- package/lib/index.js +237 -0
- package/lib/json-value.js +18 -0
- package/lib/model-file.js +48 -0
- package/lib/option-shape.js +62 -0
- package/lib/report-diff-tool.js +63 -0
- package/lib/report-view.js +433 -0
- package/lib/structure-tool.js +91 -0
- package/lib/structure.js +711 -0
- package/lib/tool.js +94 -0
- package/lib/types/baseline.d.ts +65 -0
- package/lib/types/compose-tool.d.ts +7 -0
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +37 -0
- package/lib/types/data-engine.d.ts +211 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/design-engine.d.ts +215 -0
- package/lib/types/design-tool.d.ts +11 -0
- package/lib/types/engine.d.ts +494 -0
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +37 -0
- package/lib/types/gate.d.ts +91 -0
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +45 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/model-file.d.ts +7 -0
- package/lib/types/option-shape.d.ts +22 -0
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/report-view.d.ts +50 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +152 -0
- package/lib/types/tool.d.ts +11 -0
- package/lib/types/uml-communication.d.ts +109 -0
- package/lib/types/uml-composite.d.ts +107 -0
- package/lib/types/uml-interaction-overview.d.ts +91 -0
- package/lib/types/uml-profile.d.ts +112 -0
- package/lib/types/uml-structure.d.ts +53 -0
- package/lib/types/uml-syntax.d.ts +296 -0
- package/lib/types/uml-timing.d.ts +152 -0
- package/lib/types/uml-tool.d.ts +12 -0
- package/lib/types/uml-usecase.d.ts +58 -0
- package/lib/types/uml.d.ts +303 -0
- package/lib/uml-communication.js +353 -0
- package/lib/uml-composite.js +442 -0
- package/lib/uml-interaction-overview.js +277 -0
- package/lib/uml-profile.js +373 -0
- package/lib/uml-structure.js +306 -0
- package/lib/uml-syntax.js +545 -0
- package/lib/uml-timing.js +974 -0
- package/lib/uml-tool.js +213 -0
- package/lib/uml-usecase.js +532 -0
- package/lib/uml.js +2110 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -6
- package/skills/model-everything/SKILL.md +450 -0
- package/skills/model-everything/references/dsh-model-schema.md +437 -0
- package/skills/model-everything/references/gap-routing-guide.md +42 -0
- package/skills/model-everything/references/hash-spec.md +152 -0
- package/skills/model-everything/references/logic-verification-guide.md +422 -0
- package/skills/model-everything-datamodel/SKILL.md +130 -0
- package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
- package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
- package/skills/model-everything-design/SKILL.md +115 -0
- package/skills/model-everything-interleaving/SKILL.md +63 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
- package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
- package/skills/model-everything-report-reading/SKILL.md +137 -0
- package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
- package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
- package/skills/model-everything-structure/SKILL.md +96 -0
- package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
- package/skills/model-everything-uml/SKILL.md +60 -0
- package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
- package/src/baseline.ts +288 -0
- package/src/compose-tool.ts +82 -0
- package/src/concurrency-tool.ts +44 -0
- package/src/concurrency.ts +254 -0
- package/src/data-engine.ts +1208 -0
- package/src/data-tool.ts +91 -0
- package/src/design-engine.ts +1263 -0
- package/src/design-tool.ts +68 -0
- package/src/engine.ts +4564 -0
- package/src/export-tool.ts +81 -0
- package/src/exporters.ts +399 -0
- package/src/gate.ts +330 -0
- package/src/granularity.ts +312 -0
- package/src/index.ts +289 -0
- package/src/json-value.ts +20 -0
- package/src/model-file.ts +49 -0
- package/src/option-shape.ts +64 -0
- package/src/report-diff-tool.ts +67 -0
- package/src/report-view.ts +483 -0
- package/src/structure-tool.ts +93 -0
- package/src/structure.ts +786 -0
- package/src/tool.ts +103 -0
- package/src/uml-communication.ts +428 -0
- package/src/uml-composite.ts +520 -0
- package/src/uml-interaction-overview.ts +321 -0
- package/src/uml-profile.ts +439 -0
- package/src/uml-structure.ts +317 -0
- package/src/uml-syntax.ts +764 -0
- package/src/uml-timing.ts +1106 -0
- package/src/uml-tool.ts +230 -0
- package/src/uml-usecase.ts +566 -0
- package/src/uml.ts +2139 -0
package/lib/gate.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate: one optional routing message, injected once per session.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists. Routing works through text: the measured result is that a session asked
|
|
5
|
+
* to check something loads the skill whose catalog line matches. What that measurement
|
|
6
|
+
* also showed is the limit - a session that never decides to load anything is never routed,
|
|
7
|
+
* and the catalog line is cut at 500 characters, so the tail of a long line is invisible at
|
|
8
|
+
* the moment of choosing. This component carries the routing in a message instead of in the
|
|
9
|
+
* line, for a deployment that wants the decision made for it.
|
|
10
|
+
*
|
|
11
|
+
* Why it is a separate row. The plugin's doctrine is that a capability is discoverable
|
|
12
|
+
* through its own tool description or skill text. Injection departs from that, so it is
|
|
13
|
+
* opt-in and mounted as its own component: install it, disable it, or never enable it,
|
|
14
|
+
* without touching the tools or the skills. `enabled` is a live switch, so the Web Plugins
|
|
15
|
+
* page flips it inside a running host.
|
|
16
|
+
*
|
|
17
|
+
* Why it is off by default. It costs tokens in every session that turns it on, to fix a
|
|
18
|
+
* problem text-only routing was measured not to have once the session engages. A deployment
|
|
19
|
+
* that wants it says so; the default pays nothing.
|
|
20
|
+
*
|
|
21
|
+
* What it does not do. It routes; it checks nothing. It cannot make a skill load, and it
|
|
22
|
+
* does not restate the doctrine - the skill bodies carry that, and duplicating them here
|
|
23
|
+
* would spend the session's context on a second copy.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-model-everything-gate
|
|
26
|
+
*/
|
|
27
|
+
import { readdirSync } from 'node:fs';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import z from '@deepseek-ai/schemastery';
|
|
30
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
31
|
+
export const name = 'dsh-model-everything-gate';
|
|
32
|
+
/** Producer-owned message source kind declared in `MessageSourceMap` above. */
|
|
33
|
+
const GATE_SOURCE_KIND = 'plugin:dsh-model-everything-gate';
|
|
34
|
+
/**
|
|
35
|
+
* The default text. It names every skill the package ships, because its job is the routing
|
|
36
|
+
* the catalog line cannot carry once the line is long; `tests/gate` fails when a skill is
|
|
37
|
+
* added and this list is not, which is the only thing keeping the two in step.
|
|
38
|
+
*
|
|
39
|
+
* The rule is stated at both ends, in different words. That is a measured change, not a
|
|
40
|
+
* stylistic one: the single-statement version left the one case that needed the plugin - a
|
|
41
|
+
* migration question the session believed it could answer from memory - with no skill loaded
|
|
42
|
+
* and no tool run. Stating it at the start and restating it at the end fixed that case and
|
|
43
|
+
* added no load on a control that must load nothing. Two sentences cost 403 characters; the
|
|
44
|
+
* comparison is in the routing measurement under "第三臂".
|
|
45
|
+
*/
|
|
46
|
+
/** One row per skill: the request it answers, in the words a user would use. */
|
|
47
|
+
const SKILL_ROWS = {
|
|
48
|
+
'model-everything': 'claims against code, state machines, protocols',
|
|
49
|
+
'model-everything-uml': 'drawing a diagram, or reading one someone drew',
|
|
50
|
+
'model-everything-structure': 'an architecture or dependency drawing',
|
|
51
|
+
'model-everything-design': 'layering, responsibilities, substitutability, interface width',
|
|
52
|
+
'model-everything-datamodel': 'a schema, a migration, a data guarantee',
|
|
53
|
+
'model-everything-interleaving': 'thread-safe, lock-free, ISR, async interleaving; a shared location is decided, the rest is routed',
|
|
54
|
+
'model-everything-report-reading': 'reading a report this plugin returned',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The skills this package actually ships, read from disk when the module loads.
|
|
58
|
+
*
|
|
59
|
+
* The table below advertises only these. A row for a skill that is not in the package sends the
|
|
60
|
+
* model to a name that resolves to nothing, which is the one failure a routing message must not
|
|
61
|
+
* have; a skill added without a row still gets an entry, reading "see its catalog line", so the
|
|
62
|
+
* table can be incomplete but never wrong.
|
|
63
|
+
*/
|
|
64
|
+
export function shippedSkills() {
|
|
65
|
+
try {
|
|
66
|
+
return readdirSync(fileURLToPath(new URL('../skills/', import.meta.url)), { withFileTypes: true })
|
|
67
|
+
.filter((entry) => entry.isDirectory())
|
|
68
|
+
.map((entry) => entry.name)
|
|
69
|
+
.sort();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The text, rendered from the skills that ship.
|
|
77
|
+
*
|
|
78
|
+
* The rule is stated at both ends, in different words. That is a measured change, not a stylistic
|
|
79
|
+
* one: with a single statement, the one case that needed the plugin - a migration question the
|
|
80
|
+
* session believed it could answer from memory - loaded no skill and ran no tool. Stated at the
|
|
81
|
+
* start and restated at the end, it did both, and a control task was not over-triggered.
|
|
82
|
+
*
|
|
83
|
+
* The shape follows a neighbouring plugin's injected section: a table that chooses between near-equals,
|
|
84
|
+
* rows only for what is installed, and a rule against degrading silently when a tool refuses.
|
|
85
|
+
*
|
|
86
|
+
* @param skills - the skills to advertise; defaults to the ones on disk.
|
|
87
|
+
* @param extraGuidance - a deployment's own sentence, appended last.
|
|
88
|
+
*/
|
|
89
|
+
export function renderGateText(skills = shippedSkills(), extraGuidance = '') {
|
|
90
|
+
const rows = skills.map((skill) => '| ' + (SKILL_ROWS[skill] ?? 'see its catalog line') + ' | `' + skill + '` |');
|
|
91
|
+
const extra = extraGuidance.trim();
|
|
92
|
+
return `<EXTREMELY_IMPORTANT>
|
|
93
|
+
Before answering anything about code, a design or a diagram: load the skill whose row matches, and let its tool decide. Do not answer from memory first.
|
|
94
|
+
|
|
95
|
+
Plugin dsh-model-everything is active. It checks claims about designs and code: facts against the source, behaviour as an executable model, structure as a graph.
|
|
96
|
+
|
|
97
|
+
**Unsure which row applies?** Load \`model-everything\` first: it owns the doctrine, the model schema and the routing to its siblings.
|
|
98
|
+
|
|
99
|
+
| The request | The skill |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
${rows.join('\n')}
|
|
102
|
+
|
|
103
|
+
**Tools** (dsh):
|
|
104
|
+
|
|
105
|
+
- \`model_verify\` — one state machine: structural checks, adversarial probes, budgets, deadlines, before/after regression.
|
|
106
|
+
- \`model_compose_verify\` — two or more machines: deadlock, a rendezvous that never fires, and - with \`shared\` + \`accesses\` - an unordered write (C4) or a non-atomic update (C5).
|
|
107
|
+
- \`model_datamodel_verify\` — a data model and its migration: keys, domains, migration coverage, copy consistency, rollback symmetry.
|
|
108
|
+
- \`model_design_review\` — a design against its own declarations: layering, boundaries, substitutability, interface width. The graph's shape is reported, not judged.
|
|
109
|
+
- \`model_interleaving_scan\` — finds claims about threads, synchronization, async and interrupts, names the family, and routes each to the tool that decides it. It proves nothing about any of them.
|
|
110
|
+
- \`model_export\` — UPPAAL, TLA+, PRISM or SPIN input from a verified model. It exports; it does not check the target tool.
|
|
111
|
+
- \`uml_render\`, \`uml_parse\`, \`uml_review\` — draw a model, read a diagram into one, audit one. Round-trip fidelity is measured, never assumed.
|
|
112
|
+
- \`uml_explain_labels\` — the accepted label and comment forms for one notation.
|
|
113
|
+
- \`uml_structure_verify\` — dependency review of a structure drawing, one level or several. It audits the drawing, not the code.
|
|
114
|
+
- \`report_diff\` — baseline against current: what a change added, removed or changed.
|
|
115
|
+
|
|
116
|
+
**Read a report**: gate on \`verdict\` (\`pass\`, \`pass_with_findings\`, \`fail\`), never on \`ok\`. \`verdictReason\` says why and \`nextSteps\` says what to do. \`ran\` says whether it executed, \`checksRun\` how much it examined: a \`skip\` is not a \`pass\`, and a code ending \`_UNDECIDED\` means the search hit its limit and decided nothing. Keys starting with \`_\` are notes: ignored, and left out of the model hash.
|
|
117
|
+
|
|
118
|
+
**When a tool refuses, or is not there.** Read its \`errorCode\` and fix the input before retrying; if the tool is not registered in this deployment, say so and use the general method. A check that nothing ran is not a passing check.${extra === '' ? '' : '\n\n' + extra}
|
|
119
|
+
|
|
120
|
+
The rule again, because it is the one that gets skipped: before answering anything about code, a design or a diagram, load the skill whose routing applies and let its tool decide - a claim you can check is not a claim you may answer from memory.
|
|
121
|
+
</EXTREMELY_IMPORTANT>`;
|
|
122
|
+
}
|
|
123
|
+
/** The shipped default: every row the package can advertise, no deployment guidance. */
|
|
124
|
+
export const DEFAULT_GATE_TEXT = renderGateText();
|
|
125
|
+
function live(field) {
|
|
126
|
+
const probe = field;
|
|
127
|
+
return typeof probe.volatile === 'function' ? probe.volatile() : field;
|
|
128
|
+
}
|
|
129
|
+
export const Config = z.object({
|
|
130
|
+
// On by default, and live so a deployment can turn it off without a restart. The comparison that
|
|
131
|
+
// decided this is in the routing measurement: at 2933 characters it fixed the one request class
|
|
132
|
+
// the plugin otherwise never reached, and a control task was not over-triggered.
|
|
133
|
+
enabled: live(z.boolean().default(true)),
|
|
134
|
+
// Not volatile on purpose: the settings projection feeds a GUI form, and a kilobyte of
|
|
135
|
+
// prose does not belong in one.
|
|
136
|
+
gateContent: z.string().default(DEFAULT_GATE_TEXT),
|
|
137
|
+
// A neighbour's technique, adopted: a short free-text slot a deployment can use for its own
|
|
138
|
+
// rule without forking the package. Appended last, so it is read after the routing.
|
|
139
|
+
extraGuidance: z.string().default(''),
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Read the switch as a boolean, whichever shape this host produced.
|
|
143
|
+
* @param config - the resolved plugin configuration.
|
|
144
|
+
* @returns whether the gate may be injected.
|
|
145
|
+
*/
|
|
146
|
+
function injectionEnabled(config) {
|
|
147
|
+
const value = config.enabled;
|
|
148
|
+
return typeof value === 'boolean' ? value : value.get();
|
|
149
|
+
}
|
|
150
|
+
function gateText(config) {
|
|
151
|
+
const extra = typeof config.extraGuidance === 'string' ? config.extraGuidance.trim() : '';
|
|
152
|
+
return extra === '' ? config.gateContent : config.gateContent + '\n\n' + extra;
|
|
153
|
+
}
|
|
154
|
+
function gateMessage(text) {
|
|
155
|
+
return createUserMessage({
|
|
156
|
+
content: [{ type: 'text', text }],
|
|
157
|
+
source: { kind: GATE_SOURCE_KIND },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function readSessionEvents(session) {
|
|
161
|
+
const source = session;
|
|
162
|
+
if (typeof source.snapshotEvents === 'function') {
|
|
163
|
+
const snapshot = source.snapshotEvents();
|
|
164
|
+
if (Array.isArray(snapshot))
|
|
165
|
+
return snapshot;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(source.events))
|
|
168
|
+
return source.events;
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Whether the gate already entered this session's durable history. The pre-step listener
|
|
173
|
+
* re-appends the gate until it does; once a step committed it, every later step - and a
|
|
174
|
+
* resume of a session that kept it - skips the injection. A gate dropped before any step
|
|
175
|
+
* ran simply re-injects on the first step that does.
|
|
176
|
+
* @param session - the agent's session.
|
|
177
|
+
* @returns whether a gate message is already in the log.
|
|
178
|
+
*/
|
|
179
|
+
function gateInHistory(session) {
|
|
180
|
+
return readSessionEvents(session).some((event) => {
|
|
181
|
+
if (event.type !== 'user/message')
|
|
182
|
+
return false;
|
|
183
|
+
const source = event.data?.source;
|
|
184
|
+
return source?.kind === GATE_SOURCE_KIND;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Model-visible status, so "is the gate on" is answered by inspection rather than by reading
|
|
189
|
+
* a session transcript. A separate provider id from the main row: these are two components,
|
|
190
|
+
* and one of them being mounted says nothing about the other.
|
|
191
|
+
* @param config - the resolved plugin configuration.
|
|
192
|
+
* @returns the inspect provider registration.
|
|
193
|
+
*/
|
|
194
|
+
function inspectProvider(config) {
|
|
195
|
+
return {
|
|
196
|
+
manifest: {
|
|
197
|
+
id: 'dsh-model-everything-gate',
|
|
198
|
+
description: 'The optional session-start routing message of dsh-model-everything: whether it is injected, and how much text it costs. It routes a request to a skill; it checks nothing.',
|
|
199
|
+
methods: [
|
|
200
|
+
{
|
|
201
|
+
name: 'status',
|
|
202
|
+
description: 'Read the gate status: whether injection is enabled and how long the injected text is.',
|
|
203
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
204
|
+
outputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
description: 'Gate status.',
|
|
207
|
+
properties: {
|
|
208
|
+
enabled: { type: 'boolean', description: 'Whether the gate is injected into new sessions.' },
|
|
209
|
+
gateContentLength: { type: 'integer', description: 'Length in characters of the injected text.' },
|
|
210
|
+
},
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
query: async (method) => {
|
|
217
|
+
if (method === 'status') {
|
|
218
|
+
return {
|
|
219
|
+
enabled: injectionEnabled(config),
|
|
220
|
+
gateContentLength: config.gateContent.length,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Mount the gate.
|
|
229
|
+
*
|
|
230
|
+
* The listener is registered whether or not the switch is on: the switch is volatile, so
|
|
231
|
+
* `apply` runs once and the value behind it can turn on later from the Plugins page.
|
|
232
|
+
* Returning early here would freeze the decision for the lifetime of the mount.
|
|
233
|
+
*
|
|
234
|
+
* @param ctx - the plugin context.
|
|
235
|
+
* @param config - the resolved plugin configuration.
|
|
236
|
+
*/
|
|
237
|
+
export function apply(ctx, config) {
|
|
238
|
+
// Catalog visibility is optional and the registry may be mounted after this row applies,
|
|
239
|
+
// so registration is retried on each step until it lands.
|
|
240
|
+
let providerRegistered = false;
|
|
241
|
+
const registerProvider = () => {
|
|
242
|
+
if (providerRegistered)
|
|
243
|
+
return;
|
|
244
|
+
const inspect = ctx.get('cordisInspect');
|
|
245
|
+
if (inspect === undefined)
|
|
246
|
+
return;
|
|
247
|
+
try {
|
|
248
|
+
ctx.effect(() => inspect.register(inspectProvider(config)), 'dsh-model-everything-gate: inspect provider');
|
|
249
|
+
providerRegistered = true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.warn('[dsh-model-everything-gate] inspect provider registration failed', error);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
registerProvider();
|
|
256
|
+
// Inject on the FIRST model step that runs, instead of at session start: a session-start
|
|
257
|
+
// message lands in the inbox, which a blank-session preset switch can clear before the
|
|
258
|
+
// first step. The pre-step decision is durable - the message enters history with the step,
|
|
259
|
+
// and the guard below stops it from being added a second time.
|
|
260
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
261
|
+
const decision = await next();
|
|
262
|
+
if (decision.kind === 'reject')
|
|
263
|
+
return decision;
|
|
264
|
+
registerProvider();
|
|
265
|
+
if (!injectionEnabled(config))
|
|
266
|
+
return decision;
|
|
267
|
+
if (gateInHistory(agent.session))
|
|
268
|
+
return decision;
|
|
269
|
+
return {
|
|
270
|
+
kind: 'enter',
|
|
271
|
+
messages: [...decision.messages, gateMessage(gateText(config))],
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-granularity structure review: one architecture, several levels of detail.
|
|
3
|
+
*
|
|
4
|
+
* A module diagram and the repository-wide diagram it belongs to are two views of one
|
|
5
|
+
* fact, and nothing keeps them in step — the child grows a dependency the parent never
|
|
6
|
+
* had, or the parent keeps an arrow the child quietly stopped drawing. This module
|
|
7
|
+
* checks that a child diagram is a **refinement** of its declared parent:
|
|
8
|
+
*
|
|
9
|
+
* - every dependency the child draws between two nodes the parent also has must exist in
|
|
10
|
+
* the parent (nothing appears out of nowhere);
|
|
11
|
+
* - an edge the parent draws and the child does not must be *expanded* in the child, i.e.
|
|
12
|
+
* a child path must connect the same two ends;
|
|
13
|
+
* - the declared parent relation must be a forest, not a cycle.
|
|
14
|
+
*
|
|
15
|
+
* Every per-diagram structural check still runs, and its findings are tagged with the
|
|
16
|
+
* diagram they came from (`file`), so one report can carry several diagrams without the
|
|
17
|
+
* findings becoming anonymous.
|
|
18
|
+
*
|
|
19
|
+
* @module this plugin-granularity
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { REPORT_SCHEMAS, canonicalJson, verdictOfFindings } from './engine.js';
|
|
23
|
+
import { parseStructure, reviewStructure } from './structure.js';
|
|
24
|
+
/** Reachability in one diagram, ignoring edge direction? No: a dependency path is directed. */
|
|
25
|
+
function reaches(graph, from, to) {
|
|
26
|
+
const adjacency = new Map();
|
|
27
|
+
for (const node of graph.nodes)
|
|
28
|
+
adjacency.set(node.id, []);
|
|
29
|
+
for (const edge of graph.edges)
|
|
30
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
31
|
+
const seen = new Set([from]);
|
|
32
|
+
const queue = [from];
|
|
33
|
+
while (queue.length > 0) {
|
|
34
|
+
const current = queue.shift();
|
|
35
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
36
|
+
if (next === to)
|
|
37
|
+
return true;
|
|
38
|
+
if (seen.has(next))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(next);
|
|
41
|
+
queue.push(next);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** The parent relation must be a forest: report the first cycle it contains, if any. */
|
|
47
|
+
function parentCycle(diagrams) {
|
|
48
|
+
const byName = new Map(diagrams.map((entry) => [entry.name, entry]));
|
|
49
|
+
for (const start of diagrams) {
|
|
50
|
+
const path = [start.name];
|
|
51
|
+
const seen = new Set([start.name]);
|
|
52
|
+
let cursor = start.parent;
|
|
53
|
+
while (cursor !== undefined) {
|
|
54
|
+
if (seen.has(cursor)) {
|
|
55
|
+
path.push(cursor);
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
seen.add(cursor);
|
|
59
|
+
path.push(cursor);
|
|
60
|
+
cursor = byName.get(cursor)?.parent;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Review a set of structure diagrams with declared parent relations. Per-diagram
|
|
67
|
+
* structural checks run first; the cross-diagram refinement checks follow.
|
|
68
|
+
*/
|
|
69
|
+
export function reviewGranularity(options) {
|
|
70
|
+
const diagrams = options.diagrams;
|
|
71
|
+
const findings = [];
|
|
72
|
+
const warnings = [];
|
|
73
|
+
const diagramSummaries = [];
|
|
74
|
+
const graphs = new Map();
|
|
75
|
+
if (diagrams.length === 0) {
|
|
76
|
+
findings.push({ code: 'UML031_NO_DIAGRAMS', severity: 'error', message: 'no diagram was supplied, so there are no granularity levels to compare.' });
|
|
77
|
+
}
|
|
78
|
+
const cycle = parentCycle(diagrams);
|
|
79
|
+
if (cycle !== undefined) {
|
|
80
|
+
findings.push({
|
|
81
|
+
code: 'UML030_PARENT_CYCLE',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
message: 'the declared parent relation has a cycle: ' + cycle.join(' → ') + '. A refinement hierarchy must be a forest, or "which level owns this dependency" has no answer.',
|
|
84
|
+
evidence: { cycle },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const knownNames = new Set(diagrams.map((entry) => entry.name));
|
|
88
|
+
for (const entry of diagrams) {
|
|
89
|
+
if (entry.parent !== undefined && !knownNames.has(entry.parent)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
code: 'UML028_UNKNOWN_PARENT',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
message: 'diagram "' + entry.name + '" declares parent "' + entry.parent + '", which is not part of this set.',
|
|
94
|
+
file: entry.name,
|
|
95
|
+
evidence: { diagram: entry.name, parent: entry.parent, known: [...knownNames].sort() },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const report = reviewStructure({
|
|
99
|
+
diagram: entry.diagram,
|
|
100
|
+
notation: options.notation ?? 'auto',
|
|
101
|
+
...(options.matrix === undefined ? {} : { matrix: options.matrix }),
|
|
102
|
+
});
|
|
103
|
+
graphs.set(entry.name, report.graph);
|
|
104
|
+
// Tag every finding with the diagram it came from: with several diagrams in one
|
|
105
|
+
// report, an untagged finding is unactionable.
|
|
106
|
+
for (const finding of report.findings) {
|
|
107
|
+
findings.push({ ...finding, file: entry.name, ...(finding.evidence === undefined ? {} : {}) });
|
|
108
|
+
}
|
|
109
|
+
warnings.push(...report.warnings.map((warning) => entry.name + ': ' + warning));
|
|
110
|
+
diagramSummaries.push({
|
|
111
|
+
name: entry.name,
|
|
112
|
+
...(entry.parent === undefined ? {} : { parent: entry.parent }),
|
|
113
|
+
notation: report.notation,
|
|
114
|
+
nodes: report.summary.nodes,
|
|
115
|
+
edges: report.summary.edges,
|
|
116
|
+
verdict: report.verdict,
|
|
117
|
+
findings: report.findings.length,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const pairs = [];
|
|
121
|
+
for (const child of diagrams) {
|
|
122
|
+
if (child.parent === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const parent = diagrams.find((entry) => entry.name === child.parent);
|
|
125
|
+
const parentGraph = graphs.get(child.parent);
|
|
126
|
+
const childGraph = graphs.get(child.name);
|
|
127
|
+
if (parent === undefined || parentGraph === undefined || childGraph === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const parentNodes = new Set(parentGraph.nodes.map((node) => node.id));
|
|
130
|
+
const childNodes = new Set(childGraph.nodes.map((node) => node.id));
|
|
131
|
+
const parentEdges = new Set(parentGraph.edges.map((edge) => edge.from + '\u0000' + edge.to));
|
|
132
|
+
const childEdges = new Set(childGraph.edges.map((edge) => edge.from + '\u0000' + edge.to));
|
|
133
|
+
let inheritedEdges = 0;
|
|
134
|
+
let newEdges = 0;
|
|
135
|
+
const inventedEdges = [];
|
|
136
|
+
for (const edge of childGraph.edges) {
|
|
137
|
+
const hasFrom = parentNodes.has(edge.from);
|
|
138
|
+
const hasTo = parentNodes.has(edge.to);
|
|
139
|
+
if (!hasFrom || !hasTo) {
|
|
140
|
+
newEdges += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (parentEdges.has(edge.from + '\u0000' + edge.to)) {
|
|
144
|
+
inheritedEdges += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
inventedEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
148
|
+
}
|
|
149
|
+
const expandedEdges = [];
|
|
150
|
+
const missingEdges = [];
|
|
151
|
+
for (const edge of parentGraph.edges) {
|
|
152
|
+
const key = edge.from + '\u0000' + edge.to;
|
|
153
|
+
if (childEdges.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
// The child only has to answer for a parent edge whose ends it also carries; a child
|
|
156
|
+
// may legitimately cover a subtree of the parent.
|
|
157
|
+
if (!childNodes.has(edge.from) || !childNodes.has(edge.to))
|
|
158
|
+
continue;
|
|
159
|
+
if (reaches(childGraph, edge.from, edge.to))
|
|
160
|
+
expandedEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
161
|
+
else
|
|
162
|
+
missingEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
163
|
+
}
|
|
164
|
+
if (inventedEdges.length > 0) {
|
|
165
|
+
findings.push({
|
|
166
|
+
code: 'UML029_REFINEMENT_VIOLATION',
|
|
167
|
+
severity: 'error',
|
|
168
|
+
message: 'diagram "' + child.name + '" draws ' + String(inventedEdges.length) + ' dependency(ies) between nodes its parent "' + parent.name + '" also has, but the parent does not: ' + inventedEdges.map((edge) => edge.from + ' → ' + edge.to).join(', ') + '. A refinement may add detail below the parent level, never a dependency at the parent level.',
|
|
169
|
+
file: child.name,
|
|
170
|
+
evidence: { kind: 'invented-edge', parent: parent.name, edges: inventedEdges },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (missingEdges.length > 0) {
|
|
174
|
+
findings.push({
|
|
175
|
+
code: 'UML029_REFINEMENT_VIOLATION',
|
|
176
|
+
severity: 'error',
|
|
177
|
+
message: 'diagram "' + child.name + '" drops ' + String(missingEdges.length) + ' dependency(ies) its parent "' + parent.name + '" draws, without expanding them into a path: ' + missingEdges.map((edge) => edge.from + ' → ' + edge.to).join(', ') + '.',
|
|
178
|
+
file: child.name,
|
|
179
|
+
evidence: { kind: 'unexpanded-edge', parent: parent.name, edges: missingEdges },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
pairs.push({
|
|
183
|
+
parent: parent.name,
|
|
184
|
+
child: child.name,
|
|
185
|
+
parentNodes: parentNodes.size,
|
|
186
|
+
childNodes: childNodes.size,
|
|
187
|
+
parentEdges: parentGraph.edges.length,
|
|
188
|
+
childEdges: childGraph.edges.length,
|
|
189
|
+
inheritedEdges,
|
|
190
|
+
newEdges,
|
|
191
|
+
expandedEdges,
|
|
192
|
+
missingEdges,
|
|
193
|
+
inventedEdges,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length;
|
|
197
|
+
const warningCount = findings.filter((finding) => finding.severity === 'warning').length;
|
|
198
|
+
const invented = pairs.reduce((sum, pair) => sum + pair.inventedEdges.length, 0);
|
|
199
|
+
const missing = pairs.reduce((sum, pair) => sum + pair.missingEdges.length, 0);
|
|
200
|
+
const expanded = pairs.reduce((sum, pair) => sum + pair.expandedEdges.length, 0);
|
|
201
|
+
const nextSteps = [];
|
|
202
|
+
if (errors > 0)
|
|
203
|
+
nextSteps.push('Fix the refinement violations first: a level that invents or drops a parent-level dependency makes the hierarchy unusable as a rule source.');
|
|
204
|
+
if (pairs.length === 0 && diagrams.length > 0)
|
|
205
|
+
nextSteps.push('Give the levels a `parent` so the refinement can be checked; a set of unrelated diagrams has no consistency property to verify.');
|
|
206
|
+
if (expanded > 0)
|
|
207
|
+
nextSteps.push(String(expanded) + ' parent edge(s) are drawn as a path in the child — that is a legitimate expansion, and it is listed per pair so a reviewer can confirm each one.');
|
|
208
|
+
nextSteps.push('Reconcile the levels with the source-side scan: the diagram pair is consistent or not, and neither says anything about the code until it is scanned.');
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
ran: true,
|
|
212
|
+
...verdictOfFindings(findings),
|
|
213
|
+
schema: REPORT_SCHEMAS.granularity,
|
|
214
|
+
diagrams: diagramSummaries,
|
|
215
|
+
pairs,
|
|
216
|
+
findings,
|
|
217
|
+
summary: {
|
|
218
|
+
diagrams: diagrams.length,
|
|
219
|
+
roots: diagrams.filter((entry) => entry.parent === undefined).length,
|
|
220
|
+
pairs: pairs.length,
|
|
221
|
+
nodes: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.nodes.length ?? 0), 0),
|
|
222
|
+
edges: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.edges.length ?? 0), 0),
|
|
223
|
+
inventedEdges: invented,
|
|
224
|
+
missingEdges: missing,
|
|
225
|
+
expandedEdges: expanded,
|
|
226
|
+
errors,
|
|
227
|
+
warnings: warningCount,
|
|
228
|
+
},
|
|
229
|
+
hashes: { diagrams: diagrams.map((entry) => ({ name: entry.name, hash: createHash('sha256').update(entry.diagram).digest('hex') })) },
|
|
230
|
+
warnings,
|
|
231
|
+
nextSteps,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Deterministic JSON of a granularity report's pair table, for a stable summary line. */
|
|
235
|
+
export function granularityDigest(pairs) {
|
|
236
|
+
return canonicalJson(pairs);
|
|
237
|
+
}
|