@amityco/social-plus-vise 0.14.27 → 0.14.28
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/CHANGELOG.md +41 -0
- package/README.md +3 -3
- package/dist/capabilities.js +44 -0
- package/dist/server.js +7 -1
- package/dist/tools/ast.js +161 -9
- package/dist/tools/blocks.js +95 -2
- package/dist/tools/compliance.js +15 -4
- package/dist/tools/harness.js +114 -1
- package/dist/tools/project.js +106 -11
- package/dist/tools/sdkFacts.js +83 -6
- package/dist/tools/sensors.js +30 -0
- package/package.json +7 -2
- package/scripts/extract-sdk-models.mjs +408 -0
- package/scripts/import-sdk-surface.mjs +43 -3
- package/sdk-surface/manifest.json +48 -2
- package/sdk-surface/models.android.json +990 -0
- package/sdk-surface/models.flutter.json +980 -0
- package/sdk-surface/models.ios.json +980 -0
- package/sdk-surface/models.typescript.json +1304 -0
- package/skills/social-plus-vise/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.14.28 — 2026-06-10
|
|
8
|
+
|
|
9
|
+
**Theme:** block-aware completeness — install a block, end green. Plus platform-leg hardening: Swift gets the tree-sitter treatment and iOS gets a guarded build sensor.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Field-level SDK model facts (sdk-facts Phase 4):** `vise sdk-facts` / `get_sdk_facts` no longer prove symbol existence only. The bundled snapshot now includes `sdk-surface/models.<platform>.json` (wire format `2026-06-10.sdk-model-facts.v1`, owned by `packages/schemas/model-facts.schema.json` with a type-only TS twin) carrying capability-anchored model schemas — per field: `name`, `type` (the platform's own string form), `optional`, `memberKind`, and `declaredIn` source anchor — plus full extraction provenance (extractor id/version, timestamp, upstream docs-ops extractor, upstream git commit). `--include-models` (MCP `includeModels`) serves the schemas; capability model entries gain a verbatim `schema`; every result carries a `modelFacts` status so absence is explicit.
|
|
13
|
+
- **Extraction-grounded by construction:** `scripts/extract-sdk-models.mjs` (run by `sdk-surface:import`) grounds each platform mechanically — TypeScript via the compiler API over the local SDK sources (`names-and-types`, including `?` optionality and file:line), Android via dokka signature data (`names-and-types`, Kotlin `?` nullability, doc-prose stripped), iOS/Flutter as `names-only` because their docc-abi/dart exports carry no type data (the schema then forces field `type`/`optional` to `null` — fabricated detail cannot validate). A platform whose field source is unreachable ships no model file and the manifest records why. Scope is limited to the first factory capabilities (comments, reactions, posts, users): npm tarball grew 479.2 kB → 484.2 kB.
|
|
14
|
+
- **Reaction model anchors:** the `reactions` capability now also anchors `Amity.Reactor` and `Amity.Reactable` (the reaction-summary shape blocks actually consume), alongside `Amity.Reaction`.
|
|
15
|
+
|
|
16
|
+
### Verified (model facts)
|
|
17
|
+
- `test:sdk-surface` locks the manifest `models` map (sha256, per-platform grounding) and one real extracted model per platform — including that `Amity.Comment` carries `userId` but **not** the imagined `creatorId`/`isHidden` fields that previously lived in hand-written factory fixtures.
|
|
18
|
+
- `test:sdk-facts` covers `modelFacts` status, capability schema attachment, `includeModels` capability filtering, names-only null enforcement, and the CLI `--include-models` path.
|
|
19
|
+
- The schemas package self-test validates every committed snapshot against `model-facts.schema.json` and proves the schema bites (11 doctored snapshots fail, including fabricated types under names-only grounding).
|
|
20
|
+
- **Swift AST validators (tree-sitter-swift 0.7.x):** the highest-false-positive-risk iOS rules move from regex to the same tree-sitter helper layer Kotlin uses (`tree-sitter-swift` is the maintained alex-pinkus grammar; its node shapes descend from the Kotlin grammar, so `findCallExpressions` shares the walk and adds Swift labeled-argument extraction via the new `pickLabeledArgument`). Migrated rules — ids, severities, and pass/fail semantics unchanged, only precision improved:
|
|
21
|
+
- `ios.auth.no-literal-user-id`, `ios.secret.inline-api-key`, `ios.feed.target.literal` now resolve **indirect literals** (`let FALLBACK_USER = "demo-user-42"` … `login(userId: FALLBACK_USER)`) that the call-site regex could never see — proven by the new `ios-userid-via-constant` / `ios-secret-via-constant` / `ios-feed-target-via-constant` fixtures.
|
|
22
|
+
- `ios.session-handler.retained` asks the real scope question (is the binding inside a function body?) instead of bridging 200 chars from any `func … {`. A bare class-scope `let sessionHandler = …` directly after a short function used to false-fire (the rescue pattern demanded an access modifier); the new `ios-session-handler-class-scope-bare` fixture locks the fix. Type-annotated locals (`let h: AmitySessionHandler = …`) the regex bridge missed are now caught.
|
|
23
|
+
- `ios.user.ban-state-respected` detects its interaction surface on the comment-stripped view — `// TODO: wire createPost here` no longer counts as an interaction (new `ios-ban-state-comment-only` fixture); the `// vise: ban state checked at` escape hatch deliberately stays on raw text.
|
|
24
|
+
- All Swift gate rules now use the precise tree-sitter comment stripper, chained onto the conservative scanner so bindings-unavailable/oversized files degrade to the previous behavior, never to raw text. `ios-happy-path` stays at zero findings (FP canary), and a missing `tree-sitter-swift` prebuild degrades only the Swift helpers — ts/tsx/kotlin AST stays up.
|
|
25
|
+
- **Dart AST honestly skipped:** every Dart grammar on npm fails against the pinned `tree-sitter@^0.21` (stale nan binding, ≥0.22-only API, forked core, WASM-only, or an unmaintained vendor-internal fork). Dart rules remain regex + conservative scanner; the verdict and re-evaluation trigger are recorded in ARCHITECTURE.md.
|
|
26
|
+
- **iOS build sensor — guarded best-effort (revises the old "xcodebuild is too fragile to wire" decision):** an `.xcodeproj`/`.xcworkspace` at the surface root now enables two sensors, **only when `xcodebuild` is on PATH** — absent, `vise run-sensors` reports the sensor `skipped` with the install-Xcode precondition (the pf-003 visible-precondition pattern) instead of returning no-sensors. When runnable: a cheap `xcodebuild -list` integrity probe, then a bounded-timeout build with signing disabled (`CODE_SIGNING_ALLOWED=NO`/`CODE_SIGNING_REQUIRED=NO`/`CODE_SIGN_IDENTITY=`). Non-zero exits are classified before being reported: environment-caused failures (Command-Line-Tools-only `xcode-select`, unaccepted Xcode license, signing/provisioning demands, missing simulator/SDK destination, undeterminable workspace scheme) become **skipped-with-reason and exit 0** — never project failure — while real compile/link errors stay `failed` with a non-zero CLI exit (pf-004-era semantics). The classification channel is a new serializable `environmentSkips` field on command sensors, available to any future toolchain with the same exit-code conflation. Locked by a stub-`xcodebuild` controlled-PATH matrix in `test:sensors` (detected+green, detected+real-failure, environmental skips, absent toolchain).
|
|
27
|
+
- **Installed blocks satisfy completeness baselines:** `vise check` previously scanned customer source only, so a capability delivered inside an installed Block Factory package (e.g. the Comments block's composer) still reported `completeness-gap` (exit 5). Now a checklist capability counts as present with evidence `source: "block:<blockId>"` (and a note naming the block) when an installed sidecar entry declares it and the install is still locally valid.
|
|
28
|
+
- **Factory declares, Vise validates:** Block Factory contracts declare `providesCapabilities` using Vise's completeness-checklist id vocabulary (Vise owns the id space, the factory owns the per-block claim; an empty list is legitimate — the Reactions block provides no baseline ids). `vise blocks plan`/`add` carry the registry entry's ids into the install plan and `--apply` records them — plus the manifest `dependencyName` — in `sp-vise/blocks.json` (sidecar `schemaVersion` bumped to `2026-06-10.vise-blocks-sidecar.v1`).
|
|
29
|
+
- **Seam guard:** `vise blocks plan`/`add`/`validate` emit a `blocks.providesCapabilities.known` **warning** finding (never a failure) for declared ids outside Vise's completeness vocabulary; unknown ids never satisfy a gap.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Skill: bounded-turn stop guidance.** Agents running in one-shot/print mode are told to reach a `vise check` verdict before the turn ends rather than ending mid-environment-setup (finding from the first semantic harness-E2E measurement).
|
|
33
|
+
- **Gap returns on drift:** the block evidence is registry-free and re-validated on every `vise check` — the block's dependency must still be declared in the project manifest and every recorded `filesTouched` path must exist. Remove the touched source file or the package and the capability reverts to the normal `completeness-gap`. Pre-`0.14.28` sidecar entries carry no package evidence and fail closed.
|
|
34
|
+
|
|
35
|
+
### Verified
|
|
36
|
+
- `test:ast` extended with the Swift section: grammar availability, labeled-argument call extraction, identifier→literal resolution (with the env-fallback `?? ""` and interpolated-string non-resolution cases), comment stripping that preserves `//` inside strings, function-local vs type-scope handler declarations, the three indirect-literal fixtures, and the two false-positive-kill fixtures (each verified to fire under the pre-change regexes). `ios-happy-path` asserted at zero findings; existing `ios-session-handler-local-var`/`-retained` pass/fail semantics asserted unchanged.
|
|
37
|
+
- `test:sensors` gains the stub-`xcodebuild` controlled-PATH matrix: detection with/without the toolchain, detected+green (exit 0), detected+real-build-failure (exit 1, not environment-skipped), no-simulator and CLT-only environmental skips (exit 0), and the absent-toolchain skip end to end.
|
|
38
|
+
- New `test:blocks-completeness` gate: real `blocks add --apply` against the monorepo Block Factory registry, init with answers, `check --ci` green via `block:comments` evidence, gap restored after deleting a touched file and after removing the manifest dependency, empty `providesCapabilities` tolerated end to end, and the unknown-id warning on a doctored registry.
|
|
39
|
+
- The Block Factory react target-project journey (`validate:target-projects:react`) now asserts the full install journey ends **green** with `comment-composer` satisfied by `block:comments`.
|
|
40
|
+
|
|
41
|
+
### Internal
|
|
42
|
+
- **Registry seam contract extracted to `packages/schemas`:** the block-registry wire format and the `sp-vise/blocks.json` sidecar shape now live in the root workspace package `@amityco/social-plus-schemas` (private, unpublished) — a JSON Schema (`registry.schema.json`) enforced on the factory's `validate:registry`, plus the TypeScript types formerly hand-written in `src/tools/blocks.ts`. Vise imports them **type-only** via a `workspace:*` devDependency, so nothing from the package survives into the published `dist/` (proven by the packed-tarball E2E). Type-only refactor; no behavior change.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- **Attestation evidence fingerprints no longer truncate prose-embedded paths:** the source-path extractor's extension alternation was shortest-first, so `.tsx`→`.ts`, `.json`→`.js`, and `.kts`→`.kt` inside evidence prose silently recorded no fingerprint (weakening drift detection for that attestation). Found by the harness E2E attestation stage, which now cites paths in prose as the standing regression test.
|
|
46
|
+
|
|
47
|
+
|
|
7
48
|
## 0.14.27 — 2026-06-10
|
|
8
49
|
|
|
9
50
|
**Theme:** social-plus-forge monorepo move. No runtime behavior changes.
|
package/README.md
CHANGED
|
@@ -77,9 +77,9 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
|
|
|
77
77
|
|---|---|---|---|
|
|
78
78
|
| **SDK compliance** | "this is **wrong**" | 300+ deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested. A small advisory subset surfaces as informational only and never blocks. |
|
|
79
79
|
| **Design conformance** | "this **looks off**" | extract the customer's design system into a contract, render a preview for confirmation, then check token usage | **Advisory** — `vise design check`/`preview`; never fails a build |
|
|
80
|
-
| **Feature completeness** | "this is **missing**" | Vise proposes a narrow baseline per outcome; for add-feed, pagination is mandatory, for add-comments, the composer/write affordance is mandatory, for add-chat, send plus read/unread state are mandatory, and for add-follow/profile, SDK-backed follower/following data is mandatory, while richer feed capabilities are opt-in choices from `vise plan` | **Decision gate** — `vise check` exits `completeness-gap` until each baseline capability is built or validly opted out; selected optional capabilities run separate sensors |
|
|
80
|
+
| **Feature completeness** | "this is **missing**" | Vise proposes a narrow baseline per outcome; for add-feed, pagination is mandatory, for add-comments, the composer/write affordance is mandatory, for add-chat, send plus read/unread state are mandatory, and for add-follow/profile, SDK-backed follower/following data is mandatory, while richer feed capabilities are opt-in choices from `vise plan` | **Decision gate** — `vise check` exits `completeness-gap` until each baseline capability is built, satisfied by an installed Block Factory block (evidence `source: "block:<id>"`, re-validated locally on every check), or validly opted out; selected optional capabilities run separate sensors |
|
|
81
81
|
|
|
82
|
-
Correctness is gated by deterministic rules or attestations. Baseline completeness is gated by explicit scope decisions: if a baseline capability is legitimately out of scope, record `// vise: scope-omit <id> — <reason>` and it no longer blocks. Optional feed capabilities such as image upload, poll creation, and edit post are offered during planning and become checked only after the user opts in. Conformance remains advisory because "matches the brand" is legitimately subjective. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
82
|
+
Correctness is gated by deterministic rules or attestations. Baseline completeness is gated by explicit scope decisions: if a baseline capability is legitimately out of scope, record `// vise: scope-omit <id> — <reason>` and it no longer blocks. Installed Block Factory blocks count too: the factory declares the completeness ids a block provides (`providesCapabilities`), `vise blocks add --apply` records them in `sp-vise/blocks.json`, and `vise check` honours them only while the block's manifest dependency and touched files are intact — on drift the gap returns. Optional feed capabilities such as image upload, poll creation, and edit post are offered during planning and become checked only after the user opts in. Conformance remains advisory because "matches the brand" is legitimately subjective. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
83
83
|
|
|
84
84
|
### Engagement Intelligence roadmap
|
|
85
85
|
|
|
@@ -232,7 +232,7 @@ The benchmark suite is intentionally reported with boundaries:
|
|
|
232
232
|
| **React Native** | ✅ Full | `tsc`, `npm lint`, SDK import smoke |
|
|
233
233
|
| **Flutter / Dart** | ✅ Full | `flutter analyze`, `flutter test` |
|
|
234
234
|
| **Android (Kotlin)** | ✅ Full | Gradle assemble, unit tests |
|
|
235
|
-
| **iOS (Swift)** | ✅ Full | Static rule checks fully operational. Build sensor
|
|
235
|
+
| **iOS (Swift)** | ✅ Full | Static rule checks fully operational (highest-risk rules use the Swift tree-sitter AST). Build sensor is **guarded best-effort**: an `.xcodeproj`/`.xcworkspace` enables an `xcodebuild -list` integrity probe plus a signing-disabled build — only when `xcodebuild` is on PATH (absent → skipped with the visible precondition). Environment-caused failures (no simulator runtime, Command-Line-Tools-only `xcode-select`, signing demands, unaccepted license) report **skipped-with-reason**, never project failure; a real build failure still exits non-zero. |
|
|
236
236
|
|
|
237
237
|
Each platform has dozens of rules across 10 compliance domains (feed, comments, moderation, chat, secrets, session & auth, notifications, live objects, logging hygiene, design tokens).
|
|
238
238
|
|
package/dist/capabilities.js
CHANGED
|
@@ -1187,6 +1187,50 @@ function baselineCapabilities(outcome) {
|
|
|
1187
1187
|
export function capabilityChecklist(outcome) {
|
|
1188
1188
|
return baselineCapabilities(outcome).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
|
|
1189
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The Vise-owned completeness id vocabulary — every id a Block Factory contract
|
|
1192
|
+
* may reference in `providesCapabilities`. Vise owns this id space; the factory
|
|
1193
|
+
* owns the per-block declaration (see block-factory/docs/vise-boundary.md).
|
|
1194
|
+
*/
|
|
1195
|
+
export function completenessCapabilityIds() {
|
|
1196
|
+
return new Set(CAPABILITIES.map((capability) => capability.id));
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Overlay installed-block evidence onto a source-scan completeness assessment.
|
|
1200
|
+
* A still-missing checklist capability becomes present with evidence
|
|
1201
|
+
* `source: "block:<blockId>"` when a locally validated installed block declares
|
|
1202
|
+
* it in `providesCapabilities`. Callers are responsible for the local validation
|
|
1203
|
+
* (package declared + filesTouched intact); on drift they must simply not pass
|
|
1204
|
+
* the capability here, so the normal gap returns.
|
|
1205
|
+
*/
|
|
1206
|
+
export function applyBlockProvidedCompleteness(assessment, provided) {
|
|
1207
|
+
if (provided.length === 0 || assessment.missing.length === 0) {
|
|
1208
|
+
return assessment;
|
|
1209
|
+
}
|
|
1210
|
+
const blockByCapability = new Map();
|
|
1211
|
+
for (const item of provided) {
|
|
1212
|
+
if (!blockByCapability.has(item.capabilityId)) {
|
|
1213
|
+
blockByCapability.set(item.capabilityId, item.blockId);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const present = [...assessment.present];
|
|
1217
|
+
const missing = [];
|
|
1218
|
+
for (const item of assessment.missing) {
|
|
1219
|
+
const blockId = blockByCapability.get(item.id);
|
|
1220
|
+
if (blockId) {
|
|
1221
|
+
present.push({
|
|
1222
|
+
id: item.id,
|
|
1223
|
+
label: item.label,
|
|
1224
|
+
source: `block:${blockId}`,
|
|
1225
|
+
note: `Satisfied by installed block "${blockId}" (sp-vise/blocks.json): the block package delivers this capability and its local install validation passed.`,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
missing.push(item);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return { ...assessment, present, missing };
|
|
1233
|
+
}
|
|
1190
1234
|
/** Optional feed-forward choices. These are not baseline completeness requirements. */
|
|
1191
1235
|
export function optionalCapabilityChecklist(outcome, availableIds) {
|
|
1192
1236
|
const available = availableIds ? new Set(availableIds) : undefined;
|
package/dist/server.js
CHANGED
|
@@ -342,7 +342,7 @@ async function handleCli(args) {
|
|
|
342
342
|
return "exit";
|
|
343
343
|
}
|
|
344
344
|
if (command === "sdk-facts" || command === "sdk_facts") {
|
|
345
|
-
assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols"], "sdk-facts");
|
|
345
|
+
assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols", "include-models"], "sdk-facts");
|
|
346
346
|
const format = flagValue(args, "format") ?? "json";
|
|
347
347
|
if (format !== "json") {
|
|
348
348
|
throw new Error("sdk-facts currently supports --format json only.");
|
|
@@ -352,6 +352,7 @@ async function handleCli(args) {
|
|
|
352
352
|
capability: flagValue(args, "capability"),
|
|
353
353
|
surfaceDir: flagValue(args, "surface-dir"),
|
|
354
354
|
includeSymbols: hasFlag(args, "include-symbols"),
|
|
355
|
+
includeModels: hasFlag(args, "include-models"),
|
|
355
356
|
});
|
|
356
357
|
return "exit";
|
|
357
358
|
}
|
|
@@ -776,10 +777,15 @@ Usage:
|
|
|
776
777
|
return `${packageName} sdk-facts
|
|
777
778
|
|
|
778
779
|
Read bundled SDK surface facts for social.plus Block Factory planning. Internal, projectless, and read-only.
|
|
780
|
+
Symbol facts prove existence; --include-models adds extraction-grounded field-level model schemas
|
|
781
|
+
(modelSchemas, plus capability model entries gain a schema). Platforms without grounded field data
|
|
782
|
+
report modelFacts.status=absent instead of fabricated fields.
|
|
779
783
|
|
|
780
784
|
Usage:
|
|
781
785
|
vise sdk-facts --platform typescript --capability comments --format json
|
|
786
|
+
vise sdk-facts --platform typescript --capability comments --include-models
|
|
782
787
|
vise sdk-facts --platform react-native --capability reactions --include-symbols
|
|
788
|
+
vise sdk-facts --platform android --include-models --format json
|
|
783
789
|
vise sdk-facts --platform android --surface-dir ./sdk-surface --format json`;
|
|
784
790
|
}
|
|
785
791
|
if (command === "blocks") {
|
package/dist/tools/ast.js
CHANGED
|
@@ -29,11 +29,21 @@ function loadNativeBindings() {
|
|
|
29
29
|
const ParserCtor = nodeRequire("tree-sitter");
|
|
30
30
|
const tsGrammars = nodeRequire("tree-sitter-typescript");
|
|
31
31
|
const kotlinGrammar = nodeRequire("tree-sitter-kotlin");
|
|
32
|
+
// Swift loads in its own try/catch: a missing tree-sitter-swift prebuild on an
|
|
33
|
+
// exotic platform must degrade ONLY the Swift helpers, not all AST analysis.
|
|
34
|
+
let swiftGrammar = null;
|
|
35
|
+
try {
|
|
36
|
+
swiftGrammar = nodeRequire("tree-sitter-swift");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
swiftGrammar = null;
|
|
40
|
+
}
|
|
32
41
|
nativeBindings = {
|
|
33
42
|
Parser: ParserCtor,
|
|
34
43
|
tsGrammar: tsGrammars.typescript,
|
|
35
44
|
tsxGrammar: tsGrammars.tsx,
|
|
36
45
|
kotlinGrammar,
|
|
46
|
+
swiftGrammar,
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
catch {
|
|
@@ -50,6 +60,14 @@ function loadNativeBindings() {
|
|
|
50
60
|
export function astAvailable() {
|
|
51
61
|
return loadNativeBindings() !== null;
|
|
52
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether the Swift grammar specifically is available. tree-sitter-swift is loaded
|
|
65
|
+
* independently of the core bindings (see loadNativeBindings), so Swift-only
|
|
66
|
+
* validators can check this and fall back to regex without disabling ts/tsx/kotlin.
|
|
67
|
+
*/
|
|
68
|
+
export function swiftAstAvailable() {
|
|
69
|
+
return loadNativeBindings()?.swiftGrammar != null;
|
|
70
|
+
}
|
|
53
71
|
/**
|
|
54
72
|
* Strip comments from source code using tree-sitter AST.
|
|
55
73
|
* Replaces comment spans with whitespace (preserving line structure).
|
|
@@ -101,6 +119,12 @@ function getParser(language) {
|
|
|
101
119
|
parser.setLanguage(native.tsxGrammar);
|
|
102
120
|
else if (language === "kotlin")
|
|
103
121
|
parser.setLanguage(native.kotlinGrammar);
|
|
122
|
+
else if (language === "swift") {
|
|
123
|
+
if (native.swiftGrammar == null) {
|
|
124
|
+
throw new Error("tree-sitter-swift unavailable; Swift AST analysis disabled (regex fallback in effect)");
|
|
125
|
+
}
|
|
126
|
+
parser.setLanguage(native.swiftGrammar);
|
|
127
|
+
}
|
|
104
128
|
else
|
|
105
129
|
parser.setLanguage(native.tsGrammar);
|
|
106
130
|
parsers.set(language, parser);
|
|
@@ -166,7 +190,10 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
166
190
|
results.push({ callee, node, args });
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
169
|
-
// Kotlin: call_expression = navigation_expression + call_suffix
|
|
193
|
+
// Kotlin AND Swift: call_expression = navigation_expression + call_suffix.
|
|
194
|
+
// (tree-sitter-swift descends from the Kotlin grammar, so the node names —
|
|
195
|
+
// navigation_expression / navigation_suffix / call_suffix / value_arguments —
|
|
196
|
+
// are identical; only Swift's labeled arguments add a value_argument_label.)
|
|
170
197
|
const navNode = node.namedChild(0);
|
|
171
198
|
const suffixNode = node.namedChild(1);
|
|
172
199
|
if (!navNode || !suffixNode || suffixNode.type !== "call_suffix")
|
|
@@ -181,8 +208,11 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
181
208
|
for (let i = 0; i < valArgsNode.namedChildCount; i++) {
|
|
182
209
|
const valArg = valArgsNode.namedChild(i);
|
|
183
210
|
if (valArg && valArg.type === "value_argument") {
|
|
184
|
-
// The actual expression is inside value_argument
|
|
185
|
-
|
|
211
|
+
// The actual expression is inside value_argument. Swift labeled arguments
|
|
212
|
+
// put a value_argument_label first — the value is the last named child.
|
|
213
|
+
// Kotlin behaviour (namedChild(0)) is intentionally unchanged.
|
|
214
|
+
const first = valArg.namedChild(0);
|
|
215
|
+
const expr = first?.type === "value_argument_label" ? valArg.namedChild(valArg.namedChildCount - 1) : first;
|
|
186
216
|
if (expr)
|
|
187
217
|
args.push(expr);
|
|
188
218
|
}
|
|
@@ -233,6 +263,93 @@ export function pickObjectProperty(objectNode, propertyName) {
|
|
|
233
263
|
}
|
|
234
264
|
return undefined;
|
|
235
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Pick the value of a labeled argument from a Swift call expression node.
|
|
268
|
+
* E.g., from `client.login(userId: HARDCODED, sessionHandler: handler)`,
|
|
269
|
+
* pick the value node for label "userId".
|
|
270
|
+
*
|
|
271
|
+
* Swift-shaped trees only (call_suffix → value_arguments → value_argument with a
|
|
272
|
+
* value_argument_label). Returns undefined when the label is absent.
|
|
273
|
+
*/
|
|
274
|
+
export function pickLabeledArgument(callNode, label) {
|
|
275
|
+
for (let i = 0; i < callNode.namedChildCount; i++) {
|
|
276
|
+
const suffix = callNode.namedChild(i);
|
|
277
|
+
if (!suffix || suffix.type !== "call_suffix")
|
|
278
|
+
continue;
|
|
279
|
+
const valArgs = suffix.namedChild(0);
|
|
280
|
+
if (!valArgs || valArgs.type !== "value_arguments")
|
|
281
|
+
continue;
|
|
282
|
+
for (let j = 0; j < valArgs.namedChildCount; j++) {
|
|
283
|
+
const valArg = valArgs.namedChild(j);
|
|
284
|
+
if (!valArg || valArg.type !== "value_argument")
|
|
285
|
+
continue;
|
|
286
|
+
const first = valArg.namedChild(0);
|
|
287
|
+
if (first?.type !== "value_argument_label" || first.text !== label)
|
|
288
|
+
continue;
|
|
289
|
+
const value = valArg.namedChild(valArg.namedChildCount - 1);
|
|
290
|
+
// namedChild(last) === the label itself when the argument has no value
|
|
291
|
+
// (selector-reference form) — treat as absent.
|
|
292
|
+
return value && value !== first ? value : undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Find Swift `let`/`var` declarations whose initializer text matches a pattern AND
|
|
299
|
+
* which are function-local (declared inside a function/initializer/closure/computed-
|
|
300
|
+
* property body rather than at type or file scope). Used for retention rules: a
|
|
301
|
+
* function-local binding dies with the stack frame, while a type-scope property
|
|
302
|
+
* lives with the object — a distinction regex bridges cannot make reliably.
|
|
303
|
+
*
|
|
304
|
+
* Conservative direction: a declaration only counts as local when a KNOWN
|
|
305
|
+
* function-ish ancestor encloses it, so unknown containers degrade toward
|
|
306
|
+
* "retained" (quiet), never toward a false positive.
|
|
307
|
+
*/
|
|
308
|
+
export function findSwiftFunctionLocalDeclarations(tree, initializerPattern) {
|
|
309
|
+
const results = [];
|
|
310
|
+
walkTree(tree.rootNode, (node) => {
|
|
311
|
+
if (node.type !== "property_declaration")
|
|
312
|
+
return;
|
|
313
|
+
const value = swiftPropertyInitializer(node);
|
|
314
|
+
if (!value || !initializerPattern.test(value.text))
|
|
315
|
+
return;
|
|
316
|
+
if (hasFunctionAncestor(node))
|
|
317
|
+
results.push(node);
|
|
318
|
+
});
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
const SWIFT_FUNCTION_SCOPES = new Set([
|
|
322
|
+
"function_declaration",
|
|
323
|
+
"init_declaration",
|
|
324
|
+
"deinit_declaration",
|
|
325
|
+
"lambda_literal",
|
|
326
|
+
"computed_property",
|
|
327
|
+
"computed_getter",
|
|
328
|
+
"computed_setter",
|
|
329
|
+
]);
|
|
330
|
+
function hasFunctionAncestor(node) {
|
|
331
|
+
let current = node.parent;
|
|
332
|
+
while (current) {
|
|
333
|
+
if (SWIFT_FUNCTION_SCOPES.has(current.type))
|
|
334
|
+
return true;
|
|
335
|
+
current = current.parent;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
/** The initializer expression of a Swift property_declaration (the value after `=`), if any. */
|
|
340
|
+
function swiftPropertyInitializer(node) {
|
|
341
|
+
// Shape: property_declaration = value_binding_pattern, pattern, [type_annotation], [value expr]
|
|
342
|
+
// The initializer (when present) is the last named child and is none of the structural parts.
|
|
343
|
+
const last = node.namedChild(node.namedChildCount - 1);
|
|
344
|
+
if (!last)
|
|
345
|
+
return undefined;
|
|
346
|
+
if (["value_binding_pattern", "pattern", "type_annotation", "modifiers", "attribute"].includes(last.type))
|
|
347
|
+
return undefined;
|
|
348
|
+
// computed_property is a body, not an initializer.
|
|
349
|
+
if (last.type === "computed_property")
|
|
350
|
+
return undefined;
|
|
351
|
+
return last;
|
|
352
|
+
}
|
|
236
353
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
237
354
|
function walkTree(node, visit) {
|
|
238
355
|
visit(node);
|
|
@@ -322,6 +439,25 @@ function extractStringLiteral(node) {
|
|
|
322
439
|
return text.slice(1, -1);
|
|
323
440
|
}
|
|
324
441
|
}
|
|
442
|
+
// Swift: line_string_literal contains line_str_text chunks; an interpolated
|
|
443
|
+
// string has >1 named child (line_str_text + interpolated_expression) and is
|
|
444
|
+
// NOT statically resolvable — mirror the TS template-literal treatment.
|
|
445
|
+
if (node.type === "line_string_literal") {
|
|
446
|
+
if (node.namedChildCount === 0)
|
|
447
|
+
return ""; // empty string ""
|
|
448
|
+
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "line_str_text") {
|
|
449
|
+
return node.namedChild(0).text;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
// Swift: multi-line """…""" literal. Swift semantics strip the newline after the
|
|
454
|
+
// opening and before the closing delimiter.
|
|
455
|
+
if (node.type === "multi_line_string_literal") {
|
|
456
|
+
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "multi_line_str_text") {
|
|
457
|
+
return node.namedChild(0).text.replace(/^\n/, "").replace(/\n[ \t]*$/, "");
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
325
461
|
return undefined;
|
|
326
462
|
}
|
|
327
463
|
function resolveIdentifierToLiteral(name, root) {
|
|
@@ -345,15 +481,31 @@ function resolveIdentifierToLiteral(name, root) {
|
|
|
345
481
|
// Kotlin: property_declaration with variable_declaration + string_literal
|
|
346
482
|
if (node.type === "property_declaration") {
|
|
347
483
|
const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
|
|
348
|
-
if (
|
|
484
|
+
if (varDecl) {
|
|
485
|
+
const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
|
|
486
|
+
if (!idNode || idNode.text !== name)
|
|
487
|
+
return;
|
|
488
|
+
const strLit = node.namedChildren.find((c) => c.type === "string_literal");
|
|
489
|
+
if (!strLit)
|
|
490
|
+
return;
|
|
491
|
+
const literal = extractStringLiteral(strLit);
|
|
492
|
+
if (literal !== undefined)
|
|
493
|
+
result = literal;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Swift: property_declaration with pattern → simple_identifier; the string
|
|
497
|
+
// literal must be a DIRECT child (the initializer) — a literal nested inside
|
|
498
|
+
// e.g. `env["KEY"] ?? ""` is not a static binding and must not resolve.
|
|
499
|
+
const pattern = node.namedChildren.find((c) => c.type === "pattern");
|
|
500
|
+
if (!pattern)
|
|
349
501
|
return;
|
|
350
|
-
const
|
|
351
|
-
if (!
|
|
502
|
+
const swiftId = pattern.namedChildren.find((c) => c.type === "simple_identifier");
|
|
503
|
+
if (!swiftId || swiftId.text !== name)
|
|
352
504
|
return;
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
505
|
+
const swiftLit = node.namedChildren.find((c) => c.type === "line_string_literal" || c.type === "multi_line_string_literal");
|
|
506
|
+
if (!swiftLit)
|
|
355
507
|
return;
|
|
356
|
-
const literal = extractStringLiteral(
|
|
508
|
+
const literal = extractStringLiteral(swiftLit);
|
|
357
509
|
if (literal !== undefined)
|
|
358
510
|
result = literal;
|
|
359
511
|
}
|
package/dist/tools/blocks.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { completenessCapabilityIds } from "../capabilities.js";
|
|
3
4
|
import { packageVersion } from "../version.js";
|
|
4
5
|
import { inspectProject } from "./project.js";
|
|
5
6
|
import { detectCommandSensors } from "./harness.js";
|
|
6
7
|
import { readDesignContract } from "./design.js";
|
|
8
|
+
const blocksSidecarSchemaVersion = "2026-06-10.vise-blocks-sidecar.v1";
|
|
7
9
|
const registryPlatformByVisePlatform = {
|
|
8
10
|
typescript: "react",
|
|
9
11
|
"react-native": "react-native",
|
|
@@ -21,6 +23,7 @@ export async function listRegistryBlocks(registryPath) {
|
|
|
21
23
|
status: block.status,
|
|
22
24
|
surfaces: block.surfaces,
|
|
23
25
|
requiredSdkCapabilities: block.requiredSdkCapabilities,
|
|
26
|
+
providesCapabilities: providesCapabilitiesFor(block),
|
|
24
27
|
events: block.events,
|
|
25
28
|
})),
|
|
26
29
|
};
|
|
@@ -88,7 +91,8 @@ export async function validateBlockInstall(options) {
|
|
|
88
91
|
const repoPath = requiredRepoPath(options);
|
|
89
92
|
const sidecar = await readSidecar(repoPath);
|
|
90
93
|
const installed = (sidecar?.installed ?? []).filter((entry) => !options.blockId || entry.blockId === options.blockId);
|
|
91
|
-
|
|
94
|
+
// Seed with plan findings so the providesCapabilities seam guard also surfaces in validate mode.
|
|
95
|
+
const findings = [...plan.findings];
|
|
92
96
|
if (installed.length === 0) {
|
|
93
97
|
findings.push({
|
|
94
98
|
ruleId: "blocks.sidecar.installed",
|
|
@@ -154,6 +158,18 @@ async function buildInstallPlan(options, mode) {
|
|
|
154
158
|
stopConditions.push(`Missing safe install anchor ${targetFile.anchor} in ${targetFile.path}.`);
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
// Seam guard: the factory declares providesCapabilities, but Vise owns the
|
|
162
|
+
// completeness id vocabulary. Unknown ids warn (never block) — they simply
|
|
163
|
+
// can't satisfy a completeness gap in `vise check`.
|
|
164
|
+
const providesCapabilities = providesCapabilitiesFor(block);
|
|
165
|
+
const knownCapabilityIds = completenessCapabilityIds();
|
|
166
|
+
const unknownCapabilityIds = providesCapabilities.filter((id) => !knownCapabilityIds.has(id));
|
|
167
|
+
const findings = unknownCapabilityIds.map((id) => ({
|
|
168
|
+
ruleId: "blocks.providesCapabilities.known",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: `Block ${block.blockId} declares providesCapabilities id "${id}", which is not in this Vise's completeness checklist vocabulary.`,
|
|
171
|
+
recommendation: "Unknown ids never satisfy completeness gaps. Align the Block Factory contract with Vise's capability catalog (vise owns the id space) or upgrade Vise.",
|
|
172
|
+
}));
|
|
157
173
|
return {
|
|
158
174
|
status: stopConditions.length > 0 ? "needs-review" : "ready",
|
|
159
175
|
mode,
|
|
@@ -168,11 +184,19 @@ async function buildInstallPlan(options, mode) {
|
|
|
168
184
|
packageSource: options.packageSource,
|
|
169
185
|
packageChange,
|
|
170
186
|
targetFiles,
|
|
187
|
+
providesCapabilities,
|
|
188
|
+
findings,
|
|
171
189
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
172
190
|
stopConditions,
|
|
173
191
|
sidecarPath: path.join("sp-vise", "blocks.json"),
|
|
174
192
|
};
|
|
175
193
|
}
|
|
194
|
+
function providesCapabilitiesFor(block) {
|
|
195
|
+
if (!Array.isArray(block.providesCapabilities)) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
return [...new Set(block.providesCapabilities.filter((id) => typeof id === "string" && id.trim() !== ""))];
|
|
199
|
+
}
|
|
176
200
|
async function loadRegistry(registryPath) {
|
|
177
201
|
if (!registryPath) {
|
|
178
202
|
throw new Error("blocks command requires --registry <path>.");
|
|
@@ -286,7 +310,7 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
|
|
|
286
310
|
const sidecarPath = path.join(repoPath, "sp-vise", "blocks.json");
|
|
287
311
|
const existing = await readSidecar(repoPath);
|
|
288
312
|
const sidecar = existing ?? {
|
|
289
|
-
schemaVersion:
|
|
313
|
+
schemaVersion: blocksSidecarSchemaVersion,
|
|
290
314
|
viseVersion: packageVersion,
|
|
291
315
|
generatedAt: new Date().toISOString(),
|
|
292
316
|
installed: [],
|
|
@@ -296,11 +320,14 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
|
|
|
296
320
|
blockVersion: plan.block.version,
|
|
297
321
|
platform: plan.registryPlatform,
|
|
298
322
|
packageSource,
|
|
323
|
+
dependencyName: plan.package.dependencyName,
|
|
324
|
+
providesCapabilities: plan.providesCapabilities,
|
|
299
325
|
filesTouched,
|
|
300
326
|
designContractDigest: designContract?.digest,
|
|
301
327
|
sdkFactsVersion: plan.block.version,
|
|
302
328
|
validationStatus: "installed",
|
|
303
329
|
};
|
|
330
|
+
sidecar.schemaVersion = blocksSidecarSchemaVersion;
|
|
304
331
|
sidecar.viseVersion = packageVersion;
|
|
305
332
|
sidecar.generatedAt = new Date().toISOString();
|
|
306
333
|
sidecar.installed = [...sidecar.installed.filter((item) => item.blockId !== plan.block.id), entry];
|
|
@@ -317,6 +344,72 @@ async function readSidecar(repoPath) {
|
|
|
317
344
|
return null;
|
|
318
345
|
}
|
|
319
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Registry-free evidence that installed blocks deliver completeness capabilities.
|
|
349
|
+
*
|
|
350
|
+
* `vise check` cannot assume registry access, so a sidecar entry's
|
|
351
|
+
* `providesCapabilities` only counts when the install is still locally valid:
|
|
352
|
+
* the block's manifest dependency is declared in the project's package manifest
|
|
353
|
+
* AND every recorded `filesTouched` path still exists. On drift (file removed,
|
|
354
|
+
* package gone, pre-providesCapabilities sidecar) the entry contributes nothing
|
|
355
|
+
* and the capability reverts to the normal completeness gap.
|
|
356
|
+
*/
|
|
357
|
+
export async function installedBlockProvidedCapabilities(repoPath) {
|
|
358
|
+
const repoRoot = path.resolve(repoPath);
|
|
359
|
+
const sidecar = await readSidecar(repoRoot);
|
|
360
|
+
if (!sidecar || !Array.isArray(sidecar.installed)) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
const provided = [];
|
|
364
|
+
for (const entry of sidecar.installed) {
|
|
365
|
+
const capabilities = Array.isArray(entry.providesCapabilities)
|
|
366
|
+
? entry.providesCapabilities.filter((id) => typeof id === "string" && id.trim() !== "")
|
|
367
|
+
: [];
|
|
368
|
+
if (capabilities.length === 0) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (!(await installedEntryLocallyValid(repoRoot, entry))) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
for (const capabilityId of capabilities) {
|
|
375
|
+
provided.push({ capabilityId, blockId: entry.blockId });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return provided;
|
|
379
|
+
}
|
|
380
|
+
async function installedEntryLocallyValid(repoRoot, entry) {
|
|
381
|
+
if (!(await blockDependencyDeclared(repoRoot, entry))) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
for (const touched of entry.filesTouched ?? []) {
|
|
385
|
+
if (typeof touched !== "string" || touched.trim() === "") {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (!(await exists(path.join(repoRoot, touched)))) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
async function blockDependencyDeclared(repoRoot, entry) {
|
|
395
|
+
const dependencyName = entry.dependencyName;
|
|
396
|
+
if (!dependencyName) {
|
|
397
|
+
// Pre-2026-06-10 sidecars carry no package evidence; fail closed so the gap stays.
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (entry.platform === "flutter") {
|
|
401
|
+
const source = await readFile(path.join(repoRoot, "pubspec.yaml"), "utf8").catch(() => "");
|
|
402
|
+
return new RegExp(`\\b${escapeRegExp(dependencyName)}\\s*:`).test(source);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const packageJson = await readPackageJson(path.join(repoRoot, "package.json"));
|
|
406
|
+
const devDependencies = packageJson.devDependencies;
|
|
407
|
+
return Boolean(packageJson.dependencies?.[dependencyName] ?? devDependencies?.[dependencyName]);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
320
413
|
function dependencyValue(root, packageInfo, packageSource, platform) {
|
|
321
414
|
if (!packageSource || packageSource === "npm") {
|
|
322
415
|
return platform === "flutter" ? "^0.1.0" : "^0.1.0";
|
package/dist/tools/compliance.js
CHANGED
|
@@ -2,12 +2,13 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
|
|
5
|
+
import { applyBlockProvidedCompleteness, assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
|
|
6
6
|
import { getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
|
|
7
7
|
import { contractRuleCandidatesForPublicId, hasMultipleContractRuleCandidates, productExpectationBindingForSensor, productExpectationTitle, publicProductRuleId, } from "../productExpectations.js";
|
|
8
8
|
import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
|
|
9
9
|
import { packageVersion } from "../version.js";
|
|
10
10
|
import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
|
|
11
|
+
import { installedBlockProvidedCapabilities } from "./blocks.js";
|
|
11
12
|
import { inspectProject, validateSetup } from "./project.js";
|
|
12
13
|
import { readCreativeSelection } from "./creative.js";
|
|
13
14
|
import { assessUxHarness, buildExperienceReport, buildUxHarness, readUxHarness, } from "./uxHarness.js";
|
|
@@ -737,7 +738,17 @@ export async function checkCompliance(repoPath) {
|
|
|
737
738
|
// Completeness-gap: capabilities that are neither present nor validly opted-out require an explicit decision
|
|
738
739
|
// (build it, or place // vise: scope-omit <id> — <reason>). The scope-omit escape hatch keeps this
|
|
739
740
|
// FP-free because any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
|
|
740
|
-
const
|
|
741
|
+
const sourceCompleteness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
|
|
742
|
+
// Installed Block Factory blocks deliver capabilities inside their packages,
|
|
743
|
+
// which the customer-source scan cannot see. Overlay block evidence for
|
|
744
|
+
// still-missing checklist items, but only from sidecar entries that pass
|
|
745
|
+
// registry-free local validation (manifest dependency declared + every
|
|
746
|
+
// filesTouched path intact — see installedBlockProvidedCapabilities). On
|
|
747
|
+
// local drift the evidence is withheld and the normal gap returns.
|
|
748
|
+
const blockProvided = sourceCompleteness && sourceCompleteness.missing.length > 0
|
|
749
|
+
? await installedBlockProvidedCapabilities(repoRoot).catch(() => [])
|
|
750
|
+
: [];
|
|
751
|
+
const completeness = sourceCompleteness ? applyBlockProvidedCompleteness(sourceCompleteness, blockProvided) : undefined;
|
|
741
752
|
const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
|
|
742
753
|
const selectedOptionalCapabilities = (await assessProjectSelectedOptionalCapabilities(inspection.effectiveRoot, compliance.outcome, compliance.selected_optional_capabilities ?? []).catch(() => null)) ?? undefined;
|
|
743
754
|
const hasSelectedOptionalFailures = ((selectedOptionalCapabilities?.failed.length ?? 0) > 0) || ((selectedOptionalCapabilities?.unknown.length ?? 0) > 0);
|
|
@@ -1337,7 +1348,7 @@ function sourcePathCandidatesFromString(value) {
|
|
|
1337
1348
|
if (looksLikeSourcePath(trimmed)) {
|
|
1338
1349
|
candidates.add(trimmed);
|
|
1339
1350
|
}
|
|
1340
|
-
const pathPattern = /(?:[A-Za-z0-9_@.-]+\/)+[A-Za-z0-9_@.-]+\.(?:ts|
|
|
1351
|
+
const pathPattern = /(?:[A-Za-z0-9_@.-]+\/)+[A-Za-z0-9_@.-]+\.(?:tsx|ts|jsx|json|js|kts|kt|java|dart|swift|xml|gradle|ya?ml|env|plist|pbxproj|podspec)|\b(?:Podfile|Package\.swift|pubspec\.yaml|package\.json|AndroidManifest\.xml)\b/g;
|
|
1341
1352
|
for (const match of trimmed.matchAll(pathPattern)) {
|
|
1342
1353
|
candidates.add(match[0]);
|
|
1343
1354
|
}
|
|
@@ -1346,7 +1357,7 @@ function sourcePathCandidatesFromString(value) {
|
|
|
1346
1357
|
function looksLikeSourcePath(value) {
|
|
1347
1358
|
return (/[/\\]/.test(value) ||
|
|
1348
1359
|
/^(?:Podfile|Package\.swift|pubspec\.yaml|package\.json|AndroidManifest\.xml)$/.test(value) ||
|
|
1349
|
-
/\.(?:ts|
|
|
1360
|
+
/\.(?:tsx|ts|jsx|json|js|kts|kt|java|dart|swift|xml|gradle|ya?ml|env|plist|pbxproj|podspec)(?::\d+)?$/i.test(value));
|
|
1350
1361
|
}
|
|
1351
1362
|
function cleanEvidencePathCandidate(value) {
|
|
1352
1363
|
return value
|