| name | easywos-spec |
| description | Bridges EasyWoS scan output with OpenSpec task generation for x64-to-ARM64 porting. MUST be invoked when OpenSpec generates tasks.md for changes involving porting_items. Parses EasyWoS YAML, runs hybrid spec matching (script initial screening + LLM semantic refinement), produces a matched YAML file with port_spec_ids and confidence scores, then guides tasks.md generation with dispatcher skill invocations. On invocation it FIRST runs a dependency preflight check and stops to ask the user if any dependent skill or data file is missing. Triggered via openspec/config.yaml rules.tasks. |
| tags | ["easywos","openspec","spec-matching","arm64","porting","task-generation"] |
EasyWoS-Spec: Spec Matching & Task Generation Skill
Overview
This skill orchestrates the pipeline:
EasyWoS YAML input → Script initial screening → LLM semantic refinement
→ Matched YAML output → tasks.md generation with dispatcher invocations → Unit test verification
0. Dependency Preflight Check (MUST run first)
Before doing any other work, this skill MUST verify that all dependent skills
and data files it relies on are present. Do this as the very first action when
the skill is invoked.
0.1 Required dependencies
| Dependency | Path | Role |
|---|
dispatcher-skill | skills/dispatcher-skill/SKILL.md | Layer-1 router invoked by every generated tasks.md entry |
arm64-baseline-porting | skills/arm64-baseline-porting/SKILL.md | Constraint framework for llm-freeform items and dispatcher fallback |
asm-x64-to-arm64 | skills/asm-x64-to-arm64/SKILL.md | Leaf skill — hand-written ARM64 assembly outputs (§7.1) |
sse-avx-to-neon | skills/sse-avx-to-neon/SKILL.md | Leaf skill — NEON intrinsics outputs (§7.2) |
intrinsics-x64-to-arm64 | skills/intrinsics-x64-to-arm64/SKILL.md | Leaf skill — intrinsic-mapping / arm64-limitations outputs (§7.2) |
arm64-inlineasm-to-intrinsics | skills/arm64-inlineasm-to-intrinsics/SKILL.md | Leaf skill — inline-asm override routing (dispatcher §3.4) |
| combined spec collection | skills/combined-spec-summary.yaml | Global spec table used for matching and dispatcher routing |
0.2 Check procedure
- For each row above, confirm the path exists.
- Collect the names of all missing dependencies (do not stop at the first one).
0.3 If any dependency is missing — install, then STOP and ask if still missing
If one or more dependencies are not found, do not proceed with parsing,
screening, matching, or task generation. Instead:
-
Attempt installation first. Run the following command to install all
skills from the canonical repo:
npx skills add https://github.com/qcom-WoSEcosystem/easywos-skills.git
-
Re-run the 0.2 check after installation completes.
-
If dependencies are STILL missing, stop and report the complete list of
the dependencies that remain absent, then ask the user to locate or install
them manually. For example:
Cannot run easywos-spec: the following dependent skills/files were still not
found after running `npx skills add https://github.com/qcom-WoSEcosystem/easywos-skills.git`:
- skills/dispatcher-skill/SKILL.md
- skills/sse-avx-to-neon/SKILL.md
These are required to route porting tasks and generate the matched output.
Please locate or install these dependent skills, then re-invoke easywos-spec.
Only when all dependencies in 0.1 are present may the skill continue to
Section 1.
Note: skills/combined-spec-summary.yaml is generated by
node skills/dispatcher-skill/scripts/combine-specs.js. If only this file is
missing but dispatcher-skill is present, you may tell the user it can be
regenerated with that command rather than treating it as a hard stop.
1. Input Parsing
1.1 Receive EasyWoS YAML file
Parameter: Path to EasyWoS-generated YAML file (provided as skill argument or
referenced in the OpenSpec change context).
/easywos-spec <path-to-easywos-output.yaml>
1.2 Validate and extract porting_items
- Read the YAML file
- Verify
schema: x64-to-arm64-porting is present at the top level
- Extract the
porting_items array
- If
porting_items is empty or missing, report "No porting items found" and stop
- For each porting_item, resolve its source code context:
- Read the file at
file_path (relative to the YAML file's directory)
- Extract lines specified by
code_range (e.g., 21-30 means lines 21 through 30 inclusive)
- Use the extracted content as the item's
context for all subsequent pipeline stages
Whole-file assembly items. When a porting_item's code_range covers an
entire hand-written assembly file (a large file containing many independent
kernels/procedures rather than one logical unit), it must not be matched or
dispatched as a single item. The screening script (§2) automatically
decomposes such items into per-kernel group children before matching.
See Section 1.4.
1.3 Load spec collection
Read the predefined spec collection file:
skills/combined-spec-summary.yaml
Extract all specs[].id, specs[].name, specs[].description, and
specs[].metadata.match_rules for use in the matching phase.
1.4 Whole-File Assembly Decomposition (per-kernel grouping)
When a porting_item describes an entire assembly file, treating it as one
unit breaks the pipeline in two ways:
- Match saturation — a very large file trips nearly every spec's
match_rules somewhere, so the candidate list loses all discriminating
power and every item appears to match every spec.
- Un-actionable tasks — a single task spanning thousands of lines (e.g.
"port lines 1–N" of a whole file) cannot be handed to a leaf skill, and
there is no granularity for per-kernel unit-test verification.
To handle this, the screening script performs a decomposition pre-pass.
1.4.1 Trigger policy
| Condition | Behaviour |
|---|
file_path ends in .asm / .s / .S and resolved context > 400 lines (the --threshold default) | Decompose automatically |
porting_item has segment: true | Always decompose (any asm file, any size) |
porting_item has segment: false | Never decompose (kept whole) |
--no-decompose passed to the script | Decomposition globally disabled |
Non-asm file, or already multi-range code_range, or fewer than 2 kernels found | Not decomposed (single-blob path, unchanged) |
The 400-line threshold is the default for distinguishing a whole multi-kernel
file from a single logical unit; pass --threshold N to the script to tune it
for a given project.
1.4.2 How decomposition works
-
Split on structural boundaries. The file is scanned for kernel entry
points: cglobal / cvisible (NASM x86inc dialect) or <name> PROC
(MASM). Each entry begins a kernel that runs until the next entry.
-
Aggregate variants into groups. Size/CPU variants of the same logical
kernel are merged by a normalized group key (kernelGroupKey). The
normalization rules are:
- Underscore-delimited ISA/CPU-feature suffixes (e.g.
_sse2, _avx2,
_neon, _mmx) are stripped — they denote the instruction set, not a
different kernel.
- Underscore-delimited dimension suffixes such as a
WxH block size (e.g.
_16x16, _8x8) are stripped, including macro-truncated fragments left by
templated codegen (a trailing _NNx or a dangling _).
- A trailing short batch/arity dimension such as
_x3 / _x4 is
retained — it marks a genuinely different algorithm variant, not a size.
- When the dimension is fused into the name (no underscore separator,
e.g. a trailing number glued to the base name), the name is kept as a
distinct kernel rather than merged. Such names usually denote
genuinely different routines, not size variants of one routine.
Generic examples (using a base name foo):
foo_16x16_sse2, foo_8x8_avx2 → group foo;
foo_x4_16x16 → group foo_x4;
foo8, foo16, foo32 (fused) → three separate kernels.
-
Emit one group child per group, in first-appearance order.
1.4.3 Group child shape (what the script outputs per group)
Each decomposed group appears in the script's JSON candidates as a normal
entry, plus these extra fields:
{
"porting_item_id": "<parent_id>__foo",
"parent_id": "<parent_id>",
"group": "foo",
"code_range": "440-512, 980-1040, 1600-1672",
"functions": ["foo_16x16_sse2", "foo_8x8_sse2", "..."],
"function_count": 40,
"matches": [ { "spec_id": 1, "spec_name": "<spec-name>", "rules_hit": 2, "matched_rules": ["..."] } ]
}
porting_item_id is <parent_id>__<group> — globally unique, stable, and
carries the parent for traceability.
code_range is a multi-range string (comma-separated) spanning every
variant in the group. The existing resolveContext multi-range support
(and the dispatcher) already accept this form unchanged.
- Matching runs on a capped body sample of each kernel (first ~60 lines),
not the entire kernel, so spec selection stays sharp.
1.4.4 Downstream impact
Decomposition is transparent to every later stage. The matched-YAML, dispatcher
contract, tasks.md generation, and verification stages simply see more,
smaller, individually-actionable items instead of one giant item. Each group
child becomes one porting_item in the matched YAML (§4.4) and one task / one
dispatcher invocation in tasks.md (§5.8).
2. Script Initial Screening (Script-based preliminary filtering)
2.1 Run the screening script
Execute the initial screening script against the input YAML:
node skills/easywos-spec/scripts/spec_matcher.js \
--input <easywos-yaml-path> \
--specs <combined-spec-summary-yaml-path> \
--output <candidates-json-path> \
[--threshold <lines>]
[--no-decompose]
2.2 Script behavior
The script performs:
-
Whole-file decomposition (pre-pass): For each porting_item, if it is an
assembly file over the threshold (or segment: true), split it into
per-kernel group children before matching (see Section 1.4). Each group is
then matched independently in the steps below; non-decomposed items follow
the original single-context path.
-
Scope pre-classification: For each line in a porting_item's context
(resolved from file_path + code_range in step 1.2, or the group body
sample for a decomposed group), classify it into one of:
instruction — assembly mnemonic line (push, mov, addps, stp, ldr, etc.)
declaration / perl_declaration — variable/type declarations
intrinsic_call / function_call — lines containing function call patterns
preprocessor — #include, #define, etc.
directive — assembler directives (.globl, .type, .cfi_*)
expression — fallback for all other non-comment lines
comment — lines that are purely comments (excluded from matching)
-
Scope-filtered regex matching: For each spec's match_rules, apply the
rule's pattern ONLY against lines classified under the rule's declared scope.
If scope is not specified, match against all non-comment lines.
-
Candidate list output: For each porting_item, output all specs whose
match_rules produced at least one hit, along with rules_hit count.
2.3 Script output format
See references/script-output-format.md for the JSON schema and example.
3. LLM Semantic Refinement (LLM-based final decision)
After receiving the script's candidate output, perform the following analysis
for each porting_item that has non-empty matches:
3.1 Refinement input
For each porting_item with candidates, you receive:
- The resolved source code context (from
file_path + code_range)
- The candidate spec list with
rules_hit and matched_rules
- Each matched rule's declared
scope
For a decomposed group child (a candidate carrying parent_id / group /
functions), additionally:
- Treat the group as the unit of refinement — it represents one logical kernel
family (all size/CPU variants of the same base routine), so a spec that
applies to one variant applies to the whole group.
- The matching context is a body sample of each variant, not the full
kernel. If a spec's applicability is genuinely uncertain from the sample,
resolve the group's
code_range against the source file and read more before
deciding — do not reject solely because the sample was short.
- Group children inherit
semantics / constraints / register_mapping from
their parent porting_item (the parent's fields describe the whole file's
kernel family); reference the parent when scoring.
3.2 Refinement rules
For each candidate match, verify:
-
Scope correctness: Does the matched pattern appear in the code in a context
that genuinely corresponds to the rule's declared scope? A pattern matching in a
comment, string literal, or wrong structural context is a false positive — remove it.
-
Semantic relevance: Is the spec's described migration guidance actually applicable
to this code? A pattern match on a register name that happens to appear in unrelated
logic is not a true match — remove it.
-
Multi-match combination: If multiple specs remain after filtering, determine
whether they address different aspects of the same code (→ keep all) or
redundantly overlap (→ keep the more specific one).
3.3 Output: port_spec_ids array
After refinement, output for each porting_item:
port_spec_ids: Array of confirmed spec IDs (sorted ascending), e.g., [3, 5]
- Empty array
[] if all candidates were rejected (no spec applies)
3.4 Confidence scoring
For each porting_item, assign:
match_confidence:
rules_hit: <total rules matched across all confirmed specs>
scope_verified: <true if LLM confirmed all matches are scope-correct>
llm_confidence: <"high" | "medium" | "low">
Confidence guidelines:
high: Multiple rules hit in correct scope, clear semantic match
medium: 1-2 rules hit, scope is borderline but likely correct
low: Pattern matched but semantic fit is uncertain, or scope is ambiguous
3.5 Low confidence handling
If llm_confidence: low, flag the item. It will be marked [NEEDS REVIEW] in
the generated tasks.md to indicate human verification is recommended.
4. Matched Output File Generation
4.1 Generate the matched YAML
Create a new YAML file that is identical to the input EasyWoS YAML with the
following additions to each porting_item:
port_spec_ids: [3, 5]
match_confidence:
rules_hit: 4
scope_verified: true
llm_confidence: high
For unmatched items:
port_spec_ids: []
match_confidence: null
4.2 Output file naming
<original-filename>-matched.yaml
Example: x64-to-arm64-porting.yaml → x64-to-arm64-porting-matched.yaml
4.3 Example output
See references/matched-yaml-example.md for a full example.
4.4 Decomposed group children in the matched YAML
When the screening script decomposed a whole-file item into group children,
the matched YAML replaces that one parent item with one porting_item per
group child. Each child is a self-contained porting_item:
- id: <parent_id>__foo
parent_id: <parent_id>
group: foo
title: <parent title> — foo
file_path: <path/to/kernels.asm>
code_range: "440-512, 980-1040, 1600-1672"
functions: [foo_16x16_sse2, foo_8x8_sse2, ...]
function_count: 40
port_spec_ids: [1]
match_confidence:
rules_hit: 2
scope_verified: true
llm_confidence: high
Rules:
- Keep the parent's
semantics, constraints, register_mapping (copied or
referenced) so the dispatcher data contract (§6.3) stays complete.
id MUST be the <parent_id>__<group> form the script emitted — tasks.md and
the dispatcher key off it.
- Preserve the
code_range multi-range string verbatim from the script output.
- Do NOT also emit the original parent item — it is fully replaced by its
children. (If a file produced zero kernels and was therefore not decomposed,
the original item is kept as-is.)
5. Tasks.md Generation Guide
When generating tasks.md for an OpenSpec change that involves porting, use the
matched YAML output to structure the task list.
5.1 Core logic
For each porting_item in the matched YAML, generate one task entry in tasks.md.
5.2 Task entry format
- [ ] N.M Port `<id>` (<title>) [lines <code_range>] → specs: <port_spec_ids>
`/dispatcher-skill <id> --specs <comma-separated-ids> --source <matched-yaml-path>`
5.3 Multi-spec matched task
When port_spec_ids has multiple entries:
- [ ] 3.2 Port `addition-logic` (SIMD Addition with Scalar Accumulation) [lines 327-384] → specs: [13, 15]
`/dispatcher-skill addition-logic --specs 13,15 --source x64-to-arm64-porting-matched.yaml`
The dispatcher MUST combine guidance from all listed specs.
5.4 Unmatched task (llm-freeform)
When port_spec_ids is empty:
- [ ] 3.3 Port `custom-block` (Custom Logic Block) [lines 500-520] → specs: [] (LLM freeform)
`/dispatcher-skill custom-block --mode llm-freeform --source x64-to-arm64-porting-matched.yaml`
The dispatcher invokes the arm64-baseline-porting skill as constraint framework.
5.5 Low confidence task marking
When match_confidence.llm_confidence is "low":
- [ ] [NEEDS REVIEW] 3.4 Port `ambiguous-block` (Unclear Pattern) [lines 600-620] → specs: [7]
`/dispatcher-skill ambiguous-block --specs 7 --source x64-to-arm64-porting-matched.yaml`
5.6 Task ordering
Tasks SHALL be ordered according to the porting_items array order in the matched
YAML file (which preserves the original EasyWoS scan order). Decomposed group
children appear in the parent's original position, in the group order the script
emitted, so all kernels of one file remain contiguous.
5.8 Decomposed group child task
A group child generates one task exactly like any other item, keyed by its
<parent_id>__<group> id. Surface the function count and group so the task is
self-describing, and group all children of one source file under a sub-heading:
### Port path/to/kernels.asm (decomposed: N kernel groups)
- [ ] 3.1 Port `<parent_id>__foo` (foo, 40 fns) [ranges 440-512, 980-1040, ...] → specs: [1]
`/dispatcher-skill <parent_id>__foo --specs 1 --source <project>-arm64-porting-matched.yaml`
- [ ] 3.2 Port `<parent_id>__bar` (bar, 24 fns) [ranges ...] → specs: [2,4]
`/dispatcher-skill <parent_id>__bar --specs 2,4 --source <project>-arm64-porting-matched.yaml`
The dispatcher resolves the group child by its id just like a normal item; the
multi-range code_range is passed through to the leaf skill as context
spanning every variant in the group.
5.7 Verification section
After all dispatcher invocation tasks, append a "Verification" section to tasks.md. The exact V.1–V.3 task wording depends on which flow Section 7 selects (assembly vs intrinsics vs both); V.4 and V.5 are common to both flows.
## Verification
- [ ] V.1 Generate gtest file `test_<name>.cpp` with per-porting-item fixtures and boundary condition tests (see Section 7 for the flow that matches the dispatched outputs)
- [ ] V.2 Generate `CMakeLists.txt` (asm flow: FetchContent gtest + armasm64 custom command; intrinsics flow: FetchContent gtest + test_kernels_<arch>.c static library)
- [ ] V.3 Generate `build_and_compare.bat` (see Section 7.4)
- [ ] V.4 Build ARM64: `cmake -S . -B build -A ARM64 && cmake --build build --config Release`
- [ ] V.5 Run tests: execute the gtest binary and verify all tests pass
For the intrinsics flow, V.1 must additionally produce a test_kernels_<arch>.c file with the MSVC compatibility shim block at the top (see references/unit-test-workflow-intrinsics.md — "Test Seam" section). This file IS the unit-under-test; the leaf-skill output .c files are NOT linked into the test binary directly.
6. Skilltree Dispatch Protocol
6.1 Dispatcher invocation format
Standard (with matched specs):
/dispatcher-skill <porting_item_id> --specs <comma-separated-spec-ids> --source <matched-yaml-path>
No match (freeform):
/dispatcher-skill <porting_item_id> --mode llm-freeform --source <matched-yaml-path>
6.2 Multi-spec combination
When --specs contains multiple IDs (e.g., --specs 3,5), the dispatcher MUST:
- Load spec descriptions for ALL listed IDs from the spec collection
- Synthesize a combined migration approach using guidance from each spec
- Pass the combined context to the appropriate leaf skill(s)
6.3 Data contract: dispatcher → leaf skill
The dispatcher passes to the leaf skill:
porting_item:
id: <porting_item_id>
file_path: <relative path to source file>
code_range: <line range in source file>
context: <source code resolved from file_path + code_range>
semantics: <semantic description>
constraints: <ARM64 constraints list>
register_mapping: <register mapping table>
matched_specs:
- id: 3
name: no-movemask
description: <full spec migration guidance>
- id: 5
name: no-zeroupper
description: <full spec migration guidance>
match_confidence:
rules_hit: 4
scope_verified: true
llm_confidence: high
For --mode llm-freeform, the matched_specs field is empty and the leaf skill
MUST load arm64-baseline-porting as its constraint framework.
6.4 Dispatcher routing rules
| Condition | Routing |
|---|
--specs N (single) | Route to leaf skill corresponding to spec N |
--specs N,M (multi) | Combine specs N and M guidance, route to most relevant leaf |
--mode llm-freeform | Route to generic LLM porting with arm64-baseline-porting constraints |
match_confidence.llm_confidence: low | Execute but flag output for human review |
7. Unit Test Generation Workflow
After the dispatcher executes leaf skills and produces ARM64 output, generate unit tests to verify functional correctness. One test fixture per porting item, using Google Test framework, calling the real ARM64 kernel via a clean C ABI.
Two parallel flows are supported, depending on what the leaf skills produced:
7.1 Assembly flow — for hand-written .asm outputs
Use when at least one dispatched item produced ARM64 assembly (*.asm with EXPORT directives). The test seam is the EXPORTed function itself; armasm64 assembles the .asm to .obj and CMake links it into the test binary.
For detailed workflow, see references/unit-test-workflow.md.
CMake template: references/cmake-template.md.
7.2 Intrinsics flow — for C/intrinsic outputs
Use when dispatched items produced C files using <arm_neon.h> / <arm_acle.h> intrinsics. The test seam is a sibling test_kernels_<arch>.c file that re-exposes each SIMD kernel with a clean C ABI (raw integer/pointer types, no project headers); CMake compiles that file as a static library and links it into the test binary. The MSVC ARM64 compatibility shim (<intrin.h> for CRC32, _BitScanForward64 for __builtin_ctzll) is required boilerplate.
For detailed workflow, see references/unit-test-workflow-intrinsics.md.
CMake template: references/cmake-template-intrinsics.md.
7.3 Selecting the flow per dispatched item
The dispatcher's source field on each matched spec indicates the leaf-skill family:
Leaf skill source value | Output language | Flow |
|---|
x64-to-arm64-asm-porting, x64-asm-to-arm64-asm | .asm (hand-written ARM64 assembly) | §7.1 assembly |
sse-avx-to-neon, intrinsics-x64-to-arm64 (intrinsic-mapping or arm64-limitations dimensions), arm64-baseline-porting (freeform produces C) | .c (NEON / ARMv8 intrinsics) | §7.2 intrinsics |
If a single tasks.md run produces both kinds of output, generate both test drivers (one binary per language family) — items don't share test seams across the asm/intrinsics boundary.
7.4 Build script (shared)
Both flows use the same build_and_compare.bat driver — see references/build-script-template.md. Substitute <name> with the project's test binary name; the script is identical otherwise.
7.5 Prerequisites
- Windows ARM64 machine (native execution, no emulation) or aarch64 Linux host.
- MSVC toolchain with ARM64 build tools (Visual Studio 2019+) — required for both flows. The asm flow additionally needs
armasm64 (ships with MSVC ARM64).
- CMake 3.14+ (for FetchContent).
- Internet access during first build (to fetch Google Test).