@invariant-ui/cli 0.0.0-stage → 0.1.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 +105 -0
- package/README.md +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { search } from "./search.js";
|
|
2
|
+
import { cmp } from "./system.js";
|
|
3
|
+
import { page } from "./views.js";
|
|
4
|
+
/**
|
|
5
|
+
* What is known about a pattern, derived from its facts. There is no stored pattern status (ADR 0002): a single
|
|
6
|
+
* field could disagree with the facts it summarizes. A pattern with no facts has no stated basis (`none`); one
|
|
7
|
+
* whose facts share a status has that status; otherwise it is `mixed`. Only `approved` means every fact is.
|
|
8
|
+
*/
|
|
9
|
+
export function patternStatus(p) {
|
|
10
|
+
const counts = {};
|
|
11
|
+
for (const f of Object.values(p.facts))
|
|
12
|
+
counts[f.status] = (counts[f.status] ?? 0) + 1;
|
|
13
|
+
const present = Object.keys(counts);
|
|
14
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
15
|
+
const overall = present.length === 0 ? "none" : present.length === 1 ? present[0] : "mixed";
|
|
16
|
+
return { overall, counts, total };
|
|
17
|
+
}
|
|
18
|
+
const kindOf = (id) => id.slice(0, Math.max(0, id.indexOf(":")));
|
|
19
|
+
function describe(ds, id) {
|
|
20
|
+
const name = ds.component(id)?.name ??
|
|
21
|
+
ds.pattern(id)?.name ??
|
|
22
|
+
ds.contract.tokens.find((t) => t.id === id)?.name ??
|
|
23
|
+
ds.contract.constraints.find((c) => c.id === id)?.name;
|
|
24
|
+
return { id, kind: kindOf(id), ...(name ? { name } : {}) };
|
|
25
|
+
}
|
|
26
|
+
/** Policies that make a pattern enforceable: they name it in their scope, or are based on a constraint it owns. */
|
|
27
|
+
function enforcement(ds, p) {
|
|
28
|
+
const owned = new Set(p.constraints ?? []);
|
|
29
|
+
return ds.contract.policies
|
|
30
|
+
.flatMap((policy) => {
|
|
31
|
+
const via = policy.basedOn.filter((id) => owned.has(id));
|
|
32
|
+
const scoped = (policy.scope.entities ?? []).includes(p.id);
|
|
33
|
+
if (via.length === 0 && !scoped)
|
|
34
|
+
return [];
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
policy: policy.id,
|
|
38
|
+
name: policy.name,
|
|
39
|
+
severity: policy.severity,
|
|
40
|
+
enforcement: policy.enforcement,
|
|
41
|
+
approvedBy: policy.approval.approvedBy,
|
|
42
|
+
...(via.length > 0 ? { constraints: via } : {})
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
})
|
|
46
|
+
.sort((a, b) => cmp(String(a.policy), String(b.policy)));
|
|
47
|
+
}
|
|
48
|
+
const evidenceEntry = (ds, e) => ({
|
|
49
|
+
id: e.id,
|
|
50
|
+
sourceId: e.sourceId,
|
|
51
|
+
method: e.method,
|
|
52
|
+
location: ds.location([e.id]) ?? e.sourceRef,
|
|
53
|
+
...(e.original ? { original: e.original.length > 200 ? `${e.original.slice(0, 199)}…` : e.original } : {})
|
|
54
|
+
});
|
|
55
|
+
function summary(ds, p) {
|
|
56
|
+
const status = patternStatus(p);
|
|
57
|
+
return {
|
|
58
|
+
id: p.id,
|
|
59
|
+
name: p.name,
|
|
60
|
+
category: p.category,
|
|
61
|
+
description: p.description,
|
|
62
|
+
...(p.intent ? { intent: p.intent } : {}),
|
|
63
|
+
components: p.components,
|
|
64
|
+
status: { overall: status.overall, counts: status.counts, total: status.total },
|
|
65
|
+
approved: status.overall === "approved",
|
|
66
|
+
enforcedBy: enforcement(ds, p).map((e) => e.policy)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The pattern catalog, filterable by category, derived status and text. Ordered by relevance, else by id. */
|
|
70
|
+
export function patternsView(ds, filters = {}) {
|
|
71
|
+
const all = ds.contract.patterns;
|
|
72
|
+
let rows = all;
|
|
73
|
+
if (filters.query?.trim()) {
|
|
74
|
+
const hits = search(ds, filters.query, ["pattern"], 100).hits;
|
|
75
|
+
const byId = new Map(all.map((p) => [p.id, p]));
|
|
76
|
+
rows = hits.flatMap((h) => byId.get(h.id) ?? []);
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
rows = [...all].sort((a, b) => cmp(a.id, b.id));
|
|
80
|
+
rows = rows.filter((p) => (!filters.category || p.category === filters.category) &&
|
|
81
|
+
(!filters.status || patternStatus(p).overall === filters.status));
|
|
82
|
+
const view = page(rows, filters.offset, filters.limit);
|
|
83
|
+
const notes = [];
|
|
84
|
+
if (all.length === 0)
|
|
85
|
+
notes.push("No patterns are defined. Invariant does not infer patterns from code, and no source documents one yet.");
|
|
86
|
+
else if (rows.length === 0)
|
|
87
|
+
notes.push(`No pattern matches these filters; the contract has ${all.length} pattern(s).`);
|
|
88
|
+
const unapproved = view.items.filter((p) => patternStatus(p).overall !== "approved").length;
|
|
89
|
+
if (unapproved > 0)
|
|
90
|
+
notes.push(`${unapproved} of ${view.items.length} pattern(s) are not fully approved. Each shows its status: treat documented, inferred or mixed patterns as proposals, not organizational standards.`);
|
|
91
|
+
return {
|
|
92
|
+
total: view.total,
|
|
93
|
+
offset: view.offset,
|
|
94
|
+
limit: view.limit,
|
|
95
|
+
truncated: view.truncated,
|
|
96
|
+
patterns: view.items.map((p) => summary(ds, p)),
|
|
97
|
+
notes
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function factEntries(ds, p) {
|
|
101
|
+
return Object.keys(p.facts)
|
|
102
|
+
.sort(cmp)
|
|
103
|
+
.map((path) => {
|
|
104
|
+
const fact = p.facts[path];
|
|
105
|
+
const view = ds.factView(p.facts, path);
|
|
106
|
+
return {
|
|
107
|
+
path,
|
|
108
|
+
...view,
|
|
109
|
+
evidence: ds.evidenceEntries(fact.evidenceIds).map((e) => evidenceEntry(ds, e))
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** One pattern in full: what it is made of, what is required of it, how sure we are, and where that comes from. */
|
|
114
|
+
export function patternView(ds, p) {
|
|
115
|
+
const status = patternStatus(p);
|
|
116
|
+
const fact = (path) => ds.factView(p.facts, path) ?? null;
|
|
117
|
+
const constraints = (p.constraints ?? []).map((id) => {
|
|
118
|
+
const c = ds.contract.constraints.find((x) => x.id === id);
|
|
119
|
+
return c
|
|
120
|
+
? {
|
|
121
|
+
id: c.id,
|
|
122
|
+
name: c.name,
|
|
123
|
+
kind: c.kind,
|
|
124
|
+
description: c.description,
|
|
125
|
+
...(c.details ? { details: c.details } : {}),
|
|
126
|
+
status: (ds.factView(c.facts, "description")?.status ?? null)
|
|
127
|
+
}
|
|
128
|
+
: { id, missing: true };
|
|
129
|
+
});
|
|
130
|
+
const notes = [];
|
|
131
|
+
if (status.overall !== "approved")
|
|
132
|
+
notes.push(status.overall === "none"
|
|
133
|
+
? "This pattern states no basis (no facts), so it is not approved."
|
|
134
|
+
: `This pattern is not fully approved (${status.overall}). Treat it as a proposal until it is approved.`);
|
|
135
|
+
if ((status.counts.inferred ?? 0) > 0)
|
|
136
|
+
notes.push(`${status.counts.inferred} fact(s) are inferred, with a confidence; confirm them before relying on them.`);
|
|
137
|
+
if (constraints.length === 0)
|
|
138
|
+
notes.push("No constraints are attached, so validation cannot enforce anything from this pattern.");
|
|
139
|
+
const open = ds.conflictsFor(p.id).filter((c) => c.resolution.state === "open");
|
|
140
|
+
if (open.length > 0)
|
|
141
|
+
notes.push(`${open.length} open conflict(s) concern this pattern.`);
|
|
142
|
+
const inheritanceActive = p.inheritsConstraints === true &&
|
|
143
|
+
p.appliesTo?.paths !== undefined &&
|
|
144
|
+
p.facts.appliesTo?.status === "approved" &&
|
|
145
|
+
p.facts.inheritsConstraints?.status === "approved";
|
|
146
|
+
if (p.inheritsConstraints === true && !inheritanceActive)
|
|
147
|
+
notes.push("This pattern opts in to inheriting the constraints of what it composes, but inheritance is off until its paths and the opt-in are approved.");
|
|
148
|
+
return {
|
|
149
|
+
id: p.id,
|
|
150
|
+
name: p.name,
|
|
151
|
+
category: p.category,
|
|
152
|
+
description: p.description,
|
|
153
|
+
...(p.intent ? { intent: p.intent } : {}),
|
|
154
|
+
...(p.appliesTo || p.inheritsConstraints !== undefined
|
|
155
|
+
? {
|
|
156
|
+
scope: {
|
|
157
|
+
...(p.appliesTo?.paths ? { paths: p.appliesTo.paths } : {}),
|
|
158
|
+
...(p.appliesTo?.frameworks ? { frameworks: p.appliesTo.frameworks } : {}),
|
|
159
|
+
inheritsConstraints: p.inheritsConstraints === true,
|
|
160
|
+
inheritanceActive
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
status: { overall: status.overall, counts: status.counts, total: status.total },
|
|
165
|
+
approved: status.overall === "approved",
|
|
166
|
+
components: p.components.map((id) => describe(ds, id)),
|
|
167
|
+
guidelines: p.guidelines.map((id) => ({
|
|
168
|
+
id,
|
|
169
|
+
name: ds.contract.guidelines.find((g) => g.id === id)?.name ?? null
|
|
170
|
+
})),
|
|
171
|
+
...(p.composition
|
|
172
|
+
? {
|
|
173
|
+
composition: {
|
|
174
|
+
parts: p.composition.parts.map((part) => ({
|
|
175
|
+
name: part.name,
|
|
176
|
+
role: part.role,
|
|
177
|
+
ref: describe(ds, part.ref),
|
|
178
|
+
...(part.region ? { region: part.region } : {}),
|
|
179
|
+
...(part.description ? { description: part.description } : {}),
|
|
180
|
+
fact: fact(`composition.parts.${part.name}`)
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
: {}),
|
|
185
|
+
...(p.layout ? { layout: p.layout } : {}),
|
|
186
|
+
...(p.behavior ? { behavior: p.behavior } : {}),
|
|
187
|
+
...(p.accessibility ? { accessibility: p.accessibility } : {}),
|
|
188
|
+
relationships: (p.relationships ?? []).map((r) => ({
|
|
189
|
+
id: r.id,
|
|
190
|
+
type: r.type,
|
|
191
|
+
target: describe(ds, r.target),
|
|
192
|
+
...(r.description ? { description: r.description } : {}),
|
|
193
|
+
fact: fact(`relationships.${r.id}`)
|
|
194
|
+
})),
|
|
195
|
+
constraints,
|
|
196
|
+
enforcedBy: enforcement(ds, p),
|
|
197
|
+
facts: factEntries(ds, p),
|
|
198
|
+
conflicts: ds.conflictsFor(p.id).map((c) => ({ id: c.id, type: c.type, state: c.resolution.state })),
|
|
199
|
+
notes
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const MAX_NODES = 200;
|
|
203
|
+
function outgoing(p) {
|
|
204
|
+
return [
|
|
205
|
+
...(p.relationships ?? []).map((r) => ({
|
|
206
|
+
from: p.id,
|
|
207
|
+
to: r.target,
|
|
208
|
+
type: r.type,
|
|
209
|
+
via: "relationship",
|
|
210
|
+
id: r.id,
|
|
211
|
+
fact: `relationships.${r.id}`
|
|
212
|
+
})),
|
|
213
|
+
...(p.composition?.parts ?? []).map((part) => ({
|
|
214
|
+
from: p.id,
|
|
215
|
+
to: part.ref,
|
|
216
|
+
type: "part",
|
|
217
|
+
via: "composition",
|
|
218
|
+
id: part.name,
|
|
219
|
+
role: part.role,
|
|
220
|
+
fact: `composition.parts.${part.name}`
|
|
221
|
+
}))
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The relationships around a pattern, walked breadth-first to `depth` (default 1, at most 5). Components are
|
|
226
|
+
* leaves. A node is expanded once, so cycles (`uses` may be cyclic) cannot loop. Composition parts appear as
|
|
227
|
+
* `part` edges with their role.
|
|
228
|
+
*/
|
|
229
|
+
export function patternRelationships(ds, root, options = {}) {
|
|
230
|
+
const direction = options.direction ?? "both";
|
|
231
|
+
const depth = Math.max(1, Math.min(options.depth ?? 1, 5));
|
|
232
|
+
const wanted = options.types && options.types.length > 0 ? new Set(options.types) : undefined;
|
|
233
|
+
const everyEdge = ds.contract.patterns.flatMap(outgoing);
|
|
234
|
+
const edges = new Map();
|
|
235
|
+
const seen = new Set([root.id]);
|
|
236
|
+
let frontier = [root.id];
|
|
237
|
+
let truncated = false;
|
|
238
|
+
for (let level = 0; level < depth && frontier.length > 0; level++) {
|
|
239
|
+
const next = [];
|
|
240
|
+
for (const id of frontier) {
|
|
241
|
+
const found = [
|
|
242
|
+
...(direction !== "in" ? everyEdge.filter((e) => e.from === id) : []),
|
|
243
|
+
...(direction !== "out" ? everyEdge.filter((e) => e.to === id) : [])
|
|
244
|
+
].filter((e) => !wanted || wanted.has(e.type));
|
|
245
|
+
for (const e of found) {
|
|
246
|
+
edges.set(`${e.from}|${e.type}|${e.id}|${e.to}`, e);
|
|
247
|
+
for (const end of [e.from, e.to]) {
|
|
248
|
+
if (seen.has(end))
|
|
249
|
+
continue;
|
|
250
|
+
if (seen.size >= MAX_NODES) {
|
|
251
|
+
truncated = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
seen.add(end);
|
|
255
|
+
if (end.startsWith("pattern:"))
|
|
256
|
+
next.push(end);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
frontier = next;
|
|
261
|
+
}
|
|
262
|
+
const nodes = [...seen].sort(cmp).map((id) => {
|
|
263
|
+
const p = ds.pattern(id);
|
|
264
|
+
return {
|
|
265
|
+
...describe(ds, id),
|
|
266
|
+
...(p ? { category: p.category, status: patternStatus(p).overall } : {}),
|
|
267
|
+
...(id === root.id ? { root: true } : {})
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
const list = [...edges.values()].sort((a, b) => cmp(a.from, b.from) || cmp(a.type, b.type) || cmp(a.to, b.to) || cmp(a.id, b.id));
|
|
271
|
+
return {
|
|
272
|
+
pattern: { id: root.id, name: root.name },
|
|
273
|
+
direction,
|
|
274
|
+
depth,
|
|
275
|
+
edges: list.map((e) => {
|
|
276
|
+
const from = ds.pattern(e.from);
|
|
277
|
+
const view = from && e.fact ? ds.factView(from.facts, e.fact) : undefined;
|
|
278
|
+
return {
|
|
279
|
+
from: e.from,
|
|
280
|
+
to: e.to,
|
|
281
|
+
type: e.type,
|
|
282
|
+
via: e.via,
|
|
283
|
+
id: e.id,
|
|
284
|
+
...(e.role ? { role: e.role } : {}),
|
|
285
|
+
status: (view?.status ?? null),
|
|
286
|
+
...(view?.source ? { source: view.source } : {})
|
|
287
|
+
};
|
|
288
|
+
}),
|
|
289
|
+
nodes,
|
|
290
|
+
truncated,
|
|
291
|
+
notes: list.length === 0
|
|
292
|
+
? ["This pattern has no relationships in the requested direction."]
|
|
293
|
+
: [
|
|
294
|
+
"Relationships are only what the contract states; none is inferred from name similarity. Each edge shows the status of the fact behind it."
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const dyn = (text) => ({ expression: text.replace(/\s+/g, " ").trim().slice(0, 80) });
|
|
3
|
+
function jsxValue(init, sf) {
|
|
4
|
+
if (!init)
|
|
5
|
+
return true;
|
|
6
|
+
if (ts.isStringLiteral(init))
|
|
7
|
+
return init.text;
|
|
8
|
+
if (ts.isJsxExpression(init) && init.expression) {
|
|
9
|
+
const e = init.expression;
|
|
10
|
+
if (ts.isStringLiteralLike(e))
|
|
11
|
+
return e.text;
|
|
12
|
+
if (ts.isNumericLiteral(e))
|
|
13
|
+
return Number(e.text);
|
|
14
|
+
if (e.kind === ts.SyntaxKind.TrueKeyword)
|
|
15
|
+
return true;
|
|
16
|
+
if (e.kind === ts.SyntaxKind.FalseKeyword)
|
|
17
|
+
return false;
|
|
18
|
+
if (e.kind === ts.SyntaxKind.NullKeyword)
|
|
19
|
+
return null;
|
|
20
|
+
return dyn(e.getText(sf));
|
|
21
|
+
}
|
|
22
|
+
return dyn(init.getText(sf));
|
|
23
|
+
}
|
|
24
|
+
/** JSX elements in a TypeScript/JavaScript snippet, by syntax only. Nothing is evaluated. */
|
|
25
|
+
export function scanJsx(code, filename = "snippet.tsx") {
|
|
26
|
+
const kind = /\.jsx?$/i.test(filename) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX;
|
|
27
|
+
const sf = ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, kind);
|
|
28
|
+
const uses = [];
|
|
29
|
+
const visit = (node) => {
|
|
30
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
31
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
32
|
+
const attrs = {};
|
|
33
|
+
let spread = false;
|
|
34
|
+
for (const a of opening.attributes.properties) {
|
|
35
|
+
if (ts.isJsxSpreadAttribute(a))
|
|
36
|
+
spread = true;
|
|
37
|
+
else
|
|
38
|
+
attrs[a.name.getText(sf)] = jsxValue(a.initializer, sf);
|
|
39
|
+
}
|
|
40
|
+
const hasChildren = ts.isJsxElement(node)
|
|
41
|
+
? node.children.some((c) => !(ts.isJsxText(c) && c.containsOnlyTriviaWhiteSpaces))
|
|
42
|
+
: false;
|
|
43
|
+
uses.push({
|
|
44
|
+
tag: opening.tagName.getText(sf),
|
|
45
|
+
line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1,
|
|
46
|
+
attrs,
|
|
47
|
+
hasChildren,
|
|
48
|
+
hasSpread: spread
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
ts.forEachChild(node, visit);
|
|
52
|
+
};
|
|
53
|
+
visit(sf);
|
|
54
|
+
return uses;
|
|
55
|
+
}
|
|
56
|
+
const RAW_TEXT = new Set(["script", "style"]);
|
|
57
|
+
const camel = (n) => /^(data|aria)-/.test(n) ? n : n.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
58
|
+
/**
|
|
59
|
+
* Element start tags in an HTML-like template (Vue, Angular, HTML). A single linear pass: no DOM, no
|
|
60
|
+
* expression evaluation. Event handlers, structural directives and references are skipped; bindings are
|
|
61
|
+
* kept as dynamic values.
|
|
62
|
+
*/
|
|
63
|
+
export function scanMarkup(code) {
|
|
64
|
+
const uses = [];
|
|
65
|
+
let i = 0;
|
|
66
|
+
let line = 1;
|
|
67
|
+
const n = code.length;
|
|
68
|
+
const advance = (to) => {
|
|
69
|
+
for (; i < to && i < n; i++)
|
|
70
|
+
if (code[i] === "\n")
|
|
71
|
+
line++;
|
|
72
|
+
};
|
|
73
|
+
while (i < n) {
|
|
74
|
+
const lt = code.indexOf("<", i);
|
|
75
|
+
if (lt < 0)
|
|
76
|
+
break;
|
|
77
|
+
advance(lt);
|
|
78
|
+
if (code.startsWith("<!--", i)) {
|
|
79
|
+
const end = code.indexOf("-->", i + 4);
|
|
80
|
+
advance(end < 0 ? n : end + 3);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const m = /^<([A-Za-z][A-Za-z0-9_.:-]*)/.exec(code.slice(i, i + 130));
|
|
84
|
+
if (!m) {
|
|
85
|
+
advance(i + 1);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const tag = m[1];
|
|
89
|
+
const startLine = line;
|
|
90
|
+
let j = i + m[0].length;
|
|
91
|
+
const attrs = {};
|
|
92
|
+
let spread = false;
|
|
93
|
+
let selfClosing = false;
|
|
94
|
+
while (j < n) {
|
|
95
|
+
while (j < n && /\s/.test(code[j]))
|
|
96
|
+
j++;
|
|
97
|
+
const c = code[j];
|
|
98
|
+
if (c === undefined)
|
|
99
|
+
break;
|
|
100
|
+
if (c === ">") {
|
|
101
|
+
j++;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (c === "/" && code[j + 1] === ">") {
|
|
105
|
+
selfClosing = true;
|
|
106
|
+
j += 2;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const nm = /^[^\s=>/"']+/.exec(code.slice(j, j + 200));
|
|
110
|
+
if (!nm) {
|
|
111
|
+
j++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const raw = nm[0];
|
|
115
|
+
j += raw.length;
|
|
116
|
+
let value;
|
|
117
|
+
let k = j;
|
|
118
|
+
while (k < n && /\s/.test(code[k]))
|
|
119
|
+
k++;
|
|
120
|
+
if (code[k] === "=") {
|
|
121
|
+
k++;
|
|
122
|
+
while (k < n && /\s/.test(code[k]))
|
|
123
|
+
k++;
|
|
124
|
+
const q = code[k];
|
|
125
|
+
if (q === '"' || q === "'") {
|
|
126
|
+
const end = code.indexOf(q, k + 1);
|
|
127
|
+
value = code.slice(k + 1, end < 0 ? n : end);
|
|
128
|
+
j = end < 0 ? n : end + 1;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const um = /^[^\s>]+/.exec(code.slice(k, k + 200));
|
|
132
|
+
value = um?.[0] ?? "";
|
|
133
|
+
j = k + value.length;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (/^(@|\(|v-on:|v-on$|#|\*|v-if|v-else|v-for|v-show|v-model|v-slot|ng)/.test(raw) &&
|
|
137
|
+
!/^\[/.test(raw)) {
|
|
138
|
+
if (raw === "v-bind" || raw.startsWith("v-bind="))
|
|
139
|
+
spread = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const bound = /^(:|v-bind:|\[)/.test(raw);
|
|
143
|
+
const name = camel(raw
|
|
144
|
+
.replace(/^(:|v-bind:)/, "")
|
|
145
|
+
.replace(/^\[|\]$/g, "")
|
|
146
|
+
.replace(/^attr\./, ""));
|
|
147
|
+
attrs[name] = bound ? dyn(value ?? "") : value === undefined ? true : value;
|
|
148
|
+
}
|
|
149
|
+
uses.push({
|
|
150
|
+
tag,
|
|
151
|
+
line: startLine,
|
|
152
|
+
attrs,
|
|
153
|
+
hasChildren: selfClosing ? false : undefined,
|
|
154
|
+
hasSpread: spread
|
|
155
|
+
});
|
|
156
|
+
advance(j);
|
|
157
|
+
if (RAW_TEXT.has(tag.toLowerCase()) && !selfClosing) {
|
|
158
|
+
const close = code.toLowerCase().indexOf(`</${tag.toLowerCase()}`, i);
|
|
159
|
+
advance(close < 0 ? n : close);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return uses;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { cmp, normalize } from "./system.js";
|
|
2
|
+
const words = (text) => text
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.split(/[^a-z0-9]+/)
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
/** Plural and `-ing` endings only: `buttons` and `button`, `variants` and `variant`, `loading` and `load`. */
|
|
8
|
+
const stem = (w) => w.length > 4 && w.endsWith("ies")
|
|
9
|
+
? `${w.slice(0, -3)}y`
|
|
10
|
+
: w.length > 4 && w.endsWith("ing")
|
|
11
|
+
? w.slice(0, -3)
|
|
12
|
+
: w.length > 3 && w.endsWith("s") && !w.endsWith("ss")
|
|
13
|
+
? w.slice(0, -1)
|
|
14
|
+
: w;
|
|
15
|
+
/** True when two words differ by at most one inserted, deleted or substituted letter. */
|
|
16
|
+
function oneEdit(a, b) {
|
|
17
|
+
if (Math.abs(a.length - b.length) > 1)
|
|
18
|
+
return false;
|
|
19
|
+
let i = 0;
|
|
20
|
+
let j = 0;
|
|
21
|
+
let edits = 0;
|
|
22
|
+
while (i < a.length && j < b.length) {
|
|
23
|
+
if (a[i] === b[j]) {
|
|
24
|
+
i++;
|
|
25
|
+
j++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (++edits > 1)
|
|
29
|
+
return false;
|
|
30
|
+
if (a.length > b.length)
|
|
31
|
+
i++;
|
|
32
|
+
else if (a.length < b.length)
|
|
33
|
+
j++;
|
|
34
|
+
else {
|
|
35
|
+
i++;
|
|
36
|
+
j++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return edits + (a.length - i) + (b.length - j) <= 1;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fixed, explainable ranking: name matches beat structure matches beat description matches. Matching is
|
|
43
|
+
* lexical: exact words, plural and `-ing` stems, word prefixes and one-letter typos. There are no synonyms,
|
|
44
|
+
* embeddings or models, so the same query always gives the same answer and every hit says what it matched.
|
|
45
|
+
*/
|
|
46
|
+
function score(query, fields) {
|
|
47
|
+
const qn = normalize(query);
|
|
48
|
+
const qw = words(query);
|
|
49
|
+
let total = 0;
|
|
50
|
+
const matchedOn = [];
|
|
51
|
+
for (const f of fields) {
|
|
52
|
+
if (!f.text)
|
|
53
|
+
continue;
|
|
54
|
+
const tn = normalize(f.text);
|
|
55
|
+
const tw = words(f.text);
|
|
56
|
+
let s = 0;
|
|
57
|
+
if (tn === qn)
|
|
58
|
+
s = 10;
|
|
59
|
+
else if (tn.startsWith(qn) && qn.length >= 2)
|
|
60
|
+
s = 6;
|
|
61
|
+
else if (qw.length > 0 && qw.every((w) => tw.includes(w)))
|
|
62
|
+
s = 5;
|
|
63
|
+
else if (qn.length >= 3 && tn.includes(qn))
|
|
64
|
+
s = 3;
|
|
65
|
+
else if (qw.length > 0 && qw.every((w) => tw.some((x) => stem(x) === stem(w))))
|
|
66
|
+
s = 4;
|
|
67
|
+
else if (qw.length > 0 && qw.every((w) => w.length >= 3 && tw.some((x) => x.startsWith(w))))
|
|
68
|
+
s = 2;
|
|
69
|
+
else if (qw.some((w) => w.length >= 3 && tw.includes(w)))
|
|
70
|
+
s = 1;
|
|
71
|
+
else if (qw.length > 0 && qw.every((w) => w.length >= 5 && tw.some((x) => oneEdit(w, x)))) {
|
|
72
|
+
total += f.weight * 0.5;
|
|
73
|
+
matchedOn.push(`${f.label} (approximate)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (s > 0) {
|
|
77
|
+
total += s * f.weight;
|
|
78
|
+
matchedOn.push(f.label);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { score: total, matchedOn };
|
|
82
|
+
}
|
|
83
|
+
export function search(ds, query, kinds = ["component", "token", "guideline", "pattern"], limit = 20) {
|
|
84
|
+
const hits = [];
|
|
85
|
+
const add = (kind, id, name, r) => {
|
|
86
|
+
if (r.score > 0)
|
|
87
|
+
hits.push({ kind, id, name, score: r.score, matchedOn: r.matchedOn });
|
|
88
|
+
};
|
|
89
|
+
if (kinds.includes("component"))
|
|
90
|
+
for (const c of ds.contract.components)
|
|
91
|
+
add("component", c.id, c.name, score(query, [
|
|
92
|
+
{ label: "name", text: c.name, weight: 10 },
|
|
93
|
+
{ label: "prop", text: c.props.map((p) => p.name).join(" "), weight: 3 },
|
|
94
|
+
{ label: "variant", text: c.variants.flatMap((v) => [v.name, ...v.values]).join(" "), weight: 3 },
|
|
95
|
+
{ label: "slot", text: c.slots.map((s) => s.name).join(" "), weight: 2 },
|
|
96
|
+
{ label: "event", text: c.events.map((e) => e.name).join(" "), weight: 2 },
|
|
97
|
+
{ label: "description", text: c.description ?? "", weight: 1 }
|
|
98
|
+
]));
|
|
99
|
+
if (kinds.includes("token"))
|
|
100
|
+
for (const t of ds.contract.tokens)
|
|
101
|
+
add("token", t.id, t.name, score(query, [
|
|
102
|
+
{ label: "name", text: t.name, weight: 10 },
|
|
103
|
+
{ label: "category", text: t.category, weight: 2 },
|
|
104
|
+
{ label: "value", text: typeof t.value.value === "string" ? t.value.value : "", weight: 2 },
|
|
105
|
+
{ label: "description", text: t.description ?? "", weight: 1 }
|
|
106
|
+
]));
|
|
107
|
+
if (kinds.includes("guideline"))
|
|
108
|
+
for (const g of ds.contract.guidelines)
|
|
109
|
+
add("guideline", g.id, g.name, score(query, [
|
|
110
|
+
{ label: "title", text: g.name, weight: 8 },
|
|
111
|
+
{ label: "kind", text: g.kind, weight: 1 }
|
|
112
|
+
]));
|
|
113
|
+
if (kinds.includes("pattern"))
|
|
114
|
+
for (const p of ds.contract.patterns)
|
|
115
|
+
add("pattern", p.id, p.name, score(query, [
|
|
116
|
+
{ label: "name", text: p.name, weight: 10 },
|
|
117
|
+
{ label: "intent", text: p.intent ?? "", weight: 3 },
|
|
118
|
+
{ label: "category", text: p.category, weight: 2 },
|
|
119
|
+
{
|
|
120
|
+
label: "part",
|
|
121
|
+
text: (p.composition?.parts ?? []).map((x) => `${x.name} ${x.ref}`).join(" "),
|
|
122
|
+
weight: 3
|
|
123
|
+
},
|
|
124
|
+
{ label: "region", text: (p.layout?.regions ?? []).map((r) => r.name).join(" "), weight: 2 },
|
|
125
|
+
{ label: "state", text: (p.behavior?.states ?? []).map((x) => x.name).join(" "), weight: 2 },
|
|
126
|
+
{ label: "description", text: p.description, weight: 1 }
|
|
127
|
+
]));
|
|
128
|
+
hits.sort((a, b) => b.score - a.score || cmp(a.kind, b.kind) || cmp(a.id, b.id));
|
|
129
|
+
return { total: hits.length, hits: hits.slice(0, Math.max(1, Math.min(limit, 100))) };
|
|
130
|
+
}
|
|
131
|
+
export const searchJson = (r) => r;
|
|
132
|
+
//# sourceMappingURL=search.js.map
|