canicode 0.11.0 → 0.11.1
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/README.md +16 -4
- package/dist/cli/index.js +520 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +20 -17
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +24 -4
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +24 -1
- package/package.json +1 -1
- package/skills/canicode/SKILL.md +6 -0
- package/skills/canicode-gotchas/SKILL.md +38 -59
- package/skills/canicode-roundtrip/SKILL.md +39 -260
- package/skills/canicode-roundtrip/helpers.js +287 -17
- package/skills/cursor/canicode/SKILL.md +6 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +38 -59
- package/skills/cursor/canicode-roundtrip/SKILL.md +39 -260
- package/skills/cursor/canicode-roundtrip/helpers.js +287 -17
package/dist/cli/index.js
CHANGED
|
@@ -1377,7 +1377,9 @@ var EVENTS = {
|
|
|
1377
1377
|
// cannot import `core/monitoring` directly, so the event fires through a
|
|
1378
1378
|
// caller-supplied callback. Define the typed name here so a future consumer
|
|
1379
1379
|
// has a single place to wire it up.
|
|
1380
|
-
ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped
|
|
1380
|
+
ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
|
|
1381
|
+
/** CLI `canicode roundtrip-tally` completed successfully. */
|
|
1382
|
+
ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`
|
|
1381
1383
|
};
|
|
1382
1384
|
|
|
1383
1385
|
// src/core/monitoring/capture.ts
|
|
@@ -1529,6 +1531,14 @@ function printDocsSetup() {
|
|
|
1529
1531
|
console.log(`
|
|
1530
1532
|
CANICODE SETUP GUIDE
|
|
1531
1533
|
|
|
1534
|
+
Skills at a glance: canicode-gotchas = survey answers saved locally (memo-only).
|
|
1535
|
+
canicode-roundtrip = same flow plus writes to Figma via use_figma (canvas).
|
|
1536
|
+
|
|
1537
|
+
Token safety: Do NOT paste your Figma token into Claude, Cursor, or other
|
|
1538
|
+
agent chats \u2014 transcripts can retain it. Use:
|
|
1539
|
+
FIGMA_TOKEN=figd_\u2026 npx canicode init
|
|
1540
|
+
or run \`npx canicode init\` and enter the token only at the CLI prompt.
|
|
1541
|
+
|
|
1532
1542
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1533
1543
|
1. CLI (REST API)
|
|
1534
1544
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
@@ -1556,6 +1566,8 @@ CANICODE SETUP GUIDE
|
|
|
1556
1566
|
2. CLAUDE CODE SKILLS (requires FIGMA_TOKEN)
|
|
1557
1567
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1558
1568
|
|
|
1569
|
+
(Same token safety as above \u2014 env var or interactive prompt, not chat.)
|
|
1570
|
+
|
|
1559
1571
|
Setup:
|
|
1560
1572
|
canicode init --token figd_xxxxxxxxxxxxx
|
|
1561
1573
|
(installs three skills into ./.claude/skills/ alongside the token)
|
|
@@ -1575,6 +1587,35 @@ CANICODE SETUP GUIDE
|
|
|
1575
1587
|
/canicode-gotchas <url> Run a gotcha survey
|
|
1576
1588
|
/canicode-roundtrip <url> Analyze, fix gotchas in Figma, re-analyze
|
|
1577
1589
|
|
|
1590
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1591
|
+
3. CURSOR SKILLS (requires FIGMA_TOKEN)
|
|
1592
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1593
|
+
|
|
1594
|
+
(Same token safety as above \u2014 env var or interactive prompt, not chat.)
|
|
1595
|
+
|
|
1596
|
+
Setup:
|
|
1597
|
+
canicode init --token figd_xxxxxxxxxxxxx --cursor-skills
|
|
1598
|
+
(installs Cursor copies of the three skills into ./.cursor/skills/)
|
|
1599
|
+
|
|
1600
|
+
Installed skills:
|
|
1601
|
+
canicode Lightweight CLI wrapper
|
|
1602
|
+
canicode-gotchas Standalone gotcha survey
|
|
1603
|
+
canicode-roundtrip Full analyze -> gotcha -> apply roundtrip
|
|
1604
|
+
|
|
1605
|
+
Flags:
|
|
1606
|
+
--cursor-skills Install Cursor copies of all three skills into .cursor/skills/
|
|
1607
|
+
--no-skills Skip Claude Code skills (with --cursor-skills, still installs
|
|
1608
|
+
the Cursor bundle plus the shared gotchas answer file)
|
|
1609
|
+
--force Overwrite existing skill files without prompting
|
|
1610
|
+
|
|
1611
|
+
Use (in Cursor Agent chat):
|
|
1612
|
+
@canicode <figma-url>
|
|
1613
|
+
@canicode-gotchas <figma-url> Run a gotcha survey
|
|
1614
|
+
@canicode-roundtrip <figma-url> Analyze, fix gotchas in Figma, re-analyze
|
|
1615
|
+
|
|
1616
|
+
See also: docs/CUSTOMIZATION.md#cursor-mcp-canicode (Figma MCP required for roundtrip
|
|
1617
|
+
writes; analyze-only works without it).
|
|
1618
|
+
|
|
1578
1619
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1579
1620
|
TOKEN PRIORITY
|
|
1580
1621
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
@@ -1590,6 +1631,7 @@ CANICODE SETUP GUIDE
|
|
|
1590
1631
|
CI/CD, automation -> CLI + FIGMA_TOKEN env var
|
|
1591
1632
|
Claude Code (full) -> canicode MCP server + FIGMA_TOKEN
|
|
1592
1633
|
Claude Code (light) -> /canicode skill + FIGMA_TOKEN
|
|
1634
|
+
Cursor Agent -> canicode init --cursor-skills + Figma MCP
|
|
1593
1635
|
In Figma -> Figma Plugin
|
|
1594
1636
|
Browser -> Web App (GitHub Pages)
|
|
1595
1637
|
Quick trial, offline -> CLI + JSON fixtures
|
|
@@ -3520,9 +3562,27 @@ defineRule({
|
|
|
3520
3562
|
definition: missingPrototypeDef,
|
|
3521
3563
|
check: missingPrototypeCheck
|
|
3522
3564
|
});
|
|
3565
|
+
var AcknowledgmentIntentSchema = z.object({
|
|
3566
|
+
field: z.string(),
|
|
3567
|
+
value: z.unknown(),
|
|
3568
|
+
scope: z.enum(["instance", "definition"])
|
|
3569
|
+
});
|
|
3570
|
+
var AcknowledgmentSceneWriteOutcomeSchema = z.object({
|
|
3571
|
+
result: z.enum([
|
|
3572
|
+
"succeeded",
|
|
3573
|
+
"silent-ignored",
|
|
3574
|
+
"api-rejected",
|
|
3575
|
+
"user-declined-propagation",
|
|
3576
|
+
"unknown"
|
|
3577
|
+
]),
|
|
3578
|
+
reason: z.string().optional()
|
|
3579
|
+
});
|
|
3523
3580
|
var AcknowledgmentSchema = z.object({
|
|
3524
3581
|
nodeId: z.string(),
|
|
3525
|
-
ruleId: z.string()
|
|
3582
|
+
ruleId: z.string(),
|
|
3583
|
+
intent: AcknowledgmentIntentSchema.optional(),
|
|
3584
|
+
sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
|
|
3585
|
+
codegenDirective: z.string().optional()
|
|
3526
3586
|
});
|
|
3527
3587
|
var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
|
|
3528
3588
|
function normalizeNodeId(id) {
|
|
@@ -4172,7 +4232,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4172
4232
|
}
|
|
4173
4233
|
|
|
4174
4234
|
// package.json
|
|
4175
|
-
var version2 = "0.11.
|
|
4235
|
+
var version2 = "0.11.1";
|
|
4176
4236
|
|
|
4177
4237
|
// src/core/engine/scoring.ts
|
|
4178
4238
|
function computeTotalScorePerCategory(configs) {
|
|
@@ -5660,7 +5720,7 @@ var AnalyzeOptionsSchema = z.object({
|
|
|
5660
5720
|
scope: z.enum(["page", "component"]).optional()
|
|
5661
5721
|
});
|
|
5662
5722
|
function registerAnalyze(cli2) {
|
|
5663
|
-
cli2.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").option("--json", "Output JSON results to stdout (same format as MCP)").option("--acknowledgments <path>", "(#371) Path to
|
|
5723
|
+
cli2.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").option("--json", "Output JSON results to stdout (same format as MCP)").option("--acknowledgments <path>", "(#371 / ADR-019) Path to JSON acknowledgments from canicode Figma annotations (nodeId, ruleId; optional intent / sceneWriteOutcome / codegenDirective per #444). Matching issues are flagged acknowledged and contribute half weight to density.").option("--scope <scope>", "(#404) Override analysis scope: `page` (screen/section \u2014 container bounds are required) or `component` (standalone reusable unit \u2014 root FILL is the design contract). Defaults to auto-detection from the root node type.").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign --api --token YOUR_TOKEN").example(" canicode analyze ./fixtures/my-design --output report.html").example(" canicode analyze ./fixtures/my-design --config ./my-config.json").action(async (input, rawOptions) => {
|
|
5664
5724
|
const parseResult = AnalyzeOptionsSchema.safeParse(rawOptions);
|
|
5665
5725
|
if (!parseResult.success) {
|
|
5666
5726
|
const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -6260,6 +6320,221 @@ ${msg}`);
|
|
|
6260
6320
|
}
|
|
6261
6321
|
});
|
|
6262
6322
|
}
|
|
6323
|
+
var DetectionSchema = z.literal("rule-based");
|
|
6324
|
+
var OutputChannelSchema = z.enum(["score", "annotation"]);
|
|
6325
|
+
var PersistenceIntentSchema = z.enum(["transient", "durable"]);
|
|
6326
|
+
var RulePurposeSchema = z.enum(["violation", "info-collection"]);
|
|
6327
|
+
|
|
6328
|
+
// src/core/contracts/gotcha-survey.ts
|
|
6329
|
+
var GradeSchema = z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]);
|
|
6330
|
+
var InstanceContextSchema = z.object({
|
|
6331
|
+
parentInstanceNodeId: z.string(),
|
|
6332
|
+
sourceNodeId: z.string(),
|
|
6333
|
+
sourceComponentId: z.string().optional(),
|
|
6334
|
+
sourceComponentName: z.string().optional()
|
|
6335
|
+
});
|
|
6336
|
+
var RuleApplyStrategySchema = z.enum([
|
|
6337
|
+
"property-mod",
|
|
6338
|
+
"structural-mod",
|
|
6339
|
+
"annotation",
|
|
6340
|
+
"auto-fix"
|
|
6341
|
+
]);
|
|
6342
|
+
var TargetPropertySchema = z.union([z.string(), z.array(z.string())]);
|
|
6343
|
+
var AnnotationPropertySchema = z.object({ type: z.string() });
|
|
6344
|
+
var GotchaDetectionSchema = DetectionSchema;
|
|
6345
|
+
var GotchaOutputChannelSchema = OutputChannelSchema.extract([
|
|
6346
|
+
"annotation"
|
|
6347
|
+
]);
|
|
6348
|
+
var GotchaPersistenceIntentSchema = PersistenceIntentSchema.extract([
|
|
6349
|
+
"durable"
|
|
6350
|
+
]);
|
|
6351
|
+
var GotchaSurveyQuestionSchema = z.object({
|
|
6352
|
+
nodeId: z.string(),
|
|
6353
|
+
nodeName: z.string(),
|
|
6354
|
+
ruleId: z.string(),
|
|
6355
|
+
detection: GotchaDetectionSchema,
|
|
6356
|
+
outputChannel: GotchaOutputChannelSchema,
|
|
6357
|
+
persistenceIntent: GotchaPersistenceIntentSchema,
|
|
6358
|
+
/**
|
|
6359
|
+
* #406: Classifies the triggering rule as `violation` (score-primary,
|
|
6360
|
+
* gotcha secondary) or `info-collection` (annotation-primary, score
|
|
6361
|
+
* minimal). Consumers use this to prioritize answers that collect durable
|
|
6362
|
+
* implementation context over answers that merely describe how to fix a
|
|
6363
|
+
* violation the rule will stop firing for.
|
|
6364
|
+
*/
|
|
6365
|
+
purpose: RulePurposeSchema,
|
|
6366
|
+
severity: SeveritySchema,
|
|
6367
|
+
question: z.string(),
|
|
6368
|
+
hint: z.string(),
|
|
6369
|
+
example: z.string(),
|
|
6370
|
+
instanceContext: InstanceContextSchema.optional(),
|
|
6371
|
+
applyStrategy: RuleApplyStrategySchema,
|
|
6372
|
+
targetProperty: TargetPropertySchema.optional(),
|
|
6373
|
+
annotationProperties: z.array(AnnotationPropertySchema).optional(),
|
|
6374
|
+
suggestedName: z.string().optional(),
|
|
6375
|
+
isInstanceChild: z.boolean(),
|
|
6376
|
+
sourceChildId: z.string().optional(),
|
|
6377
|
+
// #356: when this question collapses N instance-child issues that share the
|
|
6378
|
+
// same `(sourceComponentId, sourceNodeId, ruleId)` tuple, `replicas` is the
|
|
6379
|
+
// total instance count (>=2) and `replicaNodeIds` lists every instance scene
|
|
6380
|
+
// node id OTHER than the kept `nodeId`. Apply step iterates
|
|
6381
|
+
// `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
|
|
6382
|
+
// Single-instance questions omit both fields.
|
|
6383
|
+
replicas: z.number().int().min(2).optional(),
|
|
6384
|
+
replicaNodeIds: z.array(z.string()).optional()
|
|
6385
|
+
});
|
|
6386
|
+
var SurveyQuestionBatchSchema = z.object({
|
|
6387
|
+
ruleId: z.string(),
|
|
6388
|
+
/**
|
|
6389
|
+
* `true` when every member shares an answer-shape uniformly applicable to
|
|
6390
|
+
* all of them (e.g. one `min-width` value covers all FILL children).
|
|
6391
|
+
* The SKILL renders one shared prompt for `batchable: true` batches with
|
|
6392
|
+
* `questions.length >= 2`; everything else falls through to the
|
|
6393
|
+
* single-question template.
|
|
6394
|
+
*/
|
|
6395
|
+
batchable: z.boolean(),
|
|
6396
|
+
questions: z.array(GotchaSurveyQuestionSchema),
|
|
6397
|
+
/**
|
|
6398
|
+
* Sum of `max(question.replicas, 1)` across `questions`. Counts the
|
|
6399
|
+
* actual Figma scene fan-out so the SKILL can render `N instances`
|
|
6400
|
+
* accurately even when one batch member already collapses multiple
|
|
6401
|
+
* replicas via the #356 source-component dedupe.
|
|
6402
|
+
*/
|
|
6403
|
+
totalScenes: z.number().int().min(1)
|
|
6404
|
+
});
|
|
6405
|
+
var SurveyQuestionGroupSchema = z.object({
|
|
6406
|
+
/**
|
|
6407
|
+
* Shared `instanceContext` for the group, or `null` for the trailing
|
|
6408
|
+
* non-instance group. The SKILL emits the verbose "Instance note" header
|
|
6409
|
+
* once per non-null group instead of once per question (#370).
|
|
6410
|
+
*/
|
|
6411
|
+
instanceContext: InstanceContextSchema.nullable(),
|
|
6412
|
+
batches: z.array(SurveyQuestionBatchSchema)
|
|
6413
|
+
});
|
|
6414
|
+
var GroupedSurveySchema = z.object({
|
|
6415
|
+
groups: z.array(SurveyQuestionGroupSchema)
|
|
6416
|
+
});
|
|
6417
|
+
var GotchaSurveySchema = z.object({
|
|
6418
|
+
designGrade: GradeSchema,
|
|
6419
|
+
isReadyForCodeGen: z.boolean(),
|
|
6420
|
+
questions: z.array(GotchaSurveyQuestionSchema),
|
|
6421
|
+
groupedQuestions: GroupedSurveySchema,
|
|
6422
|
+
/**
|
|
6423
|
+
* #384 — canonical identifier for this design across canicode runs.
|
|
6424
|
+
* Computed by `computeDesignKey(input)` (`<fileKey>#<nodeId>` for Figma
|
|
6425
|
+
* URLs, absolute path for fixtures). The `canicode-gotchas` SKILL reads
|
|
6426
|
+
* this directly when upserting the per-design section, so the SKILL.md
|
|
6427
|
+
* prose no longer parses URLs (per ADR-016).
|
|
6428
|
+
*/
|
|
6429
|
+
designKey: z.string()
|
|
6430
|
+
});
|
|
6431
|
+
var AnswersMapSchema = z.record(
|
|
6432
|
+
z.string(),
|
|
6433
|
+
z.union([
|
|
6434
|
+
z.object({ answer: z.string() }),
|
|
6435
|
+
z.object({ skipped: z.literal(true) })
|
|
6436
|
+
])
|
|
6437
|
+
);
|
|
6438
|
+
var RenderGotchaSectionInputSchema = z.object({
|
|
6439
|
+
questions: z.array(GotchaSurveyQuestionSchema),
|
|
6440
|
+
answers: AnswersMapSchema,
|
|
6441
|
+
designName: z.string(),
|
|
6442
|
+
figmaUrl: z.string(),
|
|
6443
|
+
designKey: z.string(),
|
|
6444
|
+
designGrade: z.string(),
|
|
6445
|
+
analyzedAt: z.string(),
|
|
6446
|
+
/** Local date for the section header (`YYYY-MM-DD`). */
|
|
6447
|
+
today: z.string()
|
|
6448
|
+
});
|
|
6449
|
+
function isSkippedAnswer(nodeId, answers) {
|
|
6450
|
+
const v = answers[nodeId];
|
|
6451
|
+
if (v === void 0) return true;
|
|
6452
|
+
if ("skipped" in v && v.skipped === true) return true;
|
|
6453
|
+
if ("answer" in v) return false;
|
|
6454
|
+
return true;
|
|
6455
|
+
}
|
|
6456
|
+
function skippedCountsByRule(skippedQs) {
|
|
6457
|
+
const m = /* @__PURE__ */ new Map();
|
|
6458
|
+
for (const q of skippedQs) {
|
|
6459
|
+
m.set(q.ruleId, (m.get(q.ruleId) ?? 0) + 1);
|
|
6460
|
+
}
|
|
6461
|
+
return m;
|
|
6462
|
+
}
|
|
6463
|
+
function renderSkippedCompact(skippedQs) {
|
|
6464
|
+
const n = skippedQs.length;
|
|
6465
|
+
const counts = skippedCountsByRule(skippedQs);
|
|
6466
|
+
const lines = [`#### Skipped (${n})`, ""];
|
|
6467
|
+
const sortedRules = [...counts.keys()].sort((a, b) => a.localeCompare(b));
|
|
6468
|
+
for (const ruleId of sortedRules) {
|
|
6469
|
+
const c = counts.get(ruleId) ?? 0;
|
|
6470
|
+
lines.push(`- \`${ruleId}\` \xD7 ${c}`);
|
|
6471
|
+
}
|
|
6472
|
+
lines.push("");
|
|
6473
|
+
return lines.join("\n");
|
|
6474
|
+
}
|
|
6475
|
+
function renderInstanceContextBullet(q) {
|
|
6476
|
+
const ic = q.instanceContext;
|
|
6477
|
+
if (!ic) return null;
|
|
6478
|
+
let componentPart = "";
|
|
6479
|
+
if (ic.sourceComponentName !== void 0 && ic.sourceComponentId !== void 0) {
|
|
6480
|
+
componentPart = `, component \`${ic.sourceComponentName}\` / \`${ic.sourceComponentId}\``;
|
|
6481
|
+
} else if (ic.sourceComponentName !== void 0) {
|
|
6482
|
+
componentPart = `, component \`${ic.sourceComponentName}\``;
|
|
6483
|
+
} else if (ic.sourceComponentId !== void 0) {
|
|
6484
|
+
componentPart = `, component \`${ic.sourceComponentId}\``;
|
|
6485
|
+
}
|
|
6486
|
+
return `- **Instance context**: parent instance \`${ic.parentInstanceNodeId}\`, source node \`${ic.sourceNodeId}\`${componentPart} \u2014 roundtrip apply uses this to write on the source definition when instance overrides fail.`;
|
|
6487
|
+
}
|
|
6488
|
+
function renderGotchaSection(raw) {
|
|
6489
|
+
const input = RenderGotchaSectionInputSchema.parse(raw);
|
|
6490
|
+
const header = [
|
|
6491
|
+
`## #{{SECTION_NUMBER}} \u2014 ${input.designName} \u2014 ${input.today}`,
|
|
6492
|
+
"",
|
|
6493
|
+
`- **Figma URL**: ${input.figmaUrl}`,
|
|
6494
|
+
`- **Design key**: ${input.designKey}`,
|
|
6495
|
+
`- **Grade**: ${input.designGrade}`,
|
|
6496
|
+
`- **Analyzed at**: ${input.analyzedAt}`,
|
|
6497
|
+
"",
|
|
6498
|
+
"### Gotchas",
|
|
6499
|
+
""
|
|
6500
|
+
].join("\n");
|
|
6501
|
+
const answered = [];
|
|
6502
|
+
const skippedList = [];
|
|
6503
|
+
for (const q of input.questions) {
|
|
6504
|
+
if (isSkippedAnswer(q.nodeId, input.answers)) skippedList.push(q);
|
|
6505
|
+
else answered.push(q);
|
|
6506
|
+
}
|
|
6507
|
+
const blocks = [];
|
|
6508
|
+
for (const q of answered) {
|
|
6509
|
+
const v = input.answers[q.nodeId];
|
|
6510
|
+
if (v === void 0 || !("answer" in v)) {
|
|
6511
|
+
throw new Error(
|
|
6512
|
+
`renderGotchaSection: expected answer for nodeId ${q.nodeId} (answered set)`
|
|
6513
|
+
);
|
|
6514
|
+
}
|
|
6515
|
+
const answerLine = v.answer;
|
|
6516
|
+
const lines = [
|
|
6517
|
+
`#### ${q.ruleId} \u2014 ${q.nodeName}`,
|
|
6518
|
+
"",
|
|
6519
|
+
`- **Severity**: ${q.severity}`,
|
|
6520
|
+
`- **Node ID**: ${q.nodeId}`
|
|
6521
|
+
];
|
|
6522
|
+
const icBullet = renderInstanceContextBullet(q);
|
|
6523
|
+
if (icBullet !== null) {
|
|
6524
|
+
lines.push(icBullet);
|
|
6525
|
+
}
|
|
6526
|
+
lines.push(
|
|
6527
|
+
`- **Question**: ${q.question}`,
|
|
6528
|
+
`- **Answer**: ${answerLine}`,
|
|
6529
|
+
""
|
|
6530
|
+
);
|
|
6531
|
+
blocks.push(lines.join("\n"));
|
|
6532
|
+
}
|
|
6533
|
+
if (skippedList.length > 0) {
|
|
6534
|
+
blocks.push(renderSkippedCompact(skippedList));
|
|
6535
|
+
}
|
|
6536
|
+
return `${header}${blocks.join("")}`.replace(/\s+$/, "") + "\n";
|
|
6537
|
+
}
|
|
6263
6538
|
z.enum([
|
|
6264
6539
|
"missing",
|
|
6265
6540
|
"valid",
|
|
@@ -6376,10 +6651,26 @@ function ensureTrailingNewline(s) {
|
|
|
6376
6651
|
}
|
|
6377
6652
|
|
|
6378
6653
|
// src/cli/commands/upsert-gotcha-section.ts
|
|
6654
|
+
var AnswerSchema = z.union([
|
|
6655
|
+
z.object({ answer: z.string() }),
|
|
6656
|
+
z.object({ skipped: z.literal(true) })
|
|
6657
|
+
]);
|
|
6658
|
+
var UpsertJsonPayloadSchema = z.object({
|
|
6659
|
+
survey: GotchaSurveySchema.pick({
|
|
6660
|
+
designKey: true,
|
|
6661
|
+
designGrade: true,
|
|
6662
|
+
questions: true
|
|
6663
|
+
}),
|
|
6664
|
+
answers: z.record(z.string(), AnswerSchema),
|
|
6665
|
+
designName: z.string(),
|
|
6666
|
+
figmaUrl: z.string(),
|
|
6667
|
+
analyzedAt: z.string(),
|
|
6668
|
+
today: z.string()
|
|
6669
|
+
});
|
|
6379
6670
|
var UpsertOptionsSchema = z.object({
|
|
6380
6671
|
file: z.string().min(1, "--file is required"),
|
|
6381
6672
|
designKey: z.string().min(1, "--design-key is required"),
|
|
6382
|
-
|
|
6673
|
+
input: z.string().min(1, "--input is required (use '--input=-' to read stdin)")
|
|
6383
6674
|
});
|
|
6384
6675
|
var USER_MESSAGES = {
|
|
6385
6676
|
missing: "Gotchas SKILL.md not found at the given path. Run `canicode init` first, then re-invoke this skill.",
|
|
@@ -6392,8 +6683,38 @@ async function readStdin() {
|
|
|
6392
6683
|
}
|
|
6393
6684
|
return Buffer.concat(chunks).toString("utf-8");
|
|
6394
6685
|
}
|
|
6686
|
+
function parseUpsertPayload(rawJson) {
|
|
6687
|
+
let parsed;
|
|
6688
|
+
try {
|
|
6689
|
+
parsed = JSON.parse(rawJson);
|
|
6690
|
+
} catch {
|
|
6691
|
+
throw new Error("Invalid JSON in --input");
|
|
6692
|
+
}
|
|
6693
|
+
const result = UpsertJsonPayloadSchema.safeParse(parsed);
|
|
6694
|
+
if (!result.success) {
|
|
6695
|
+
const msg = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
6696
|
+
throw new Error(`Invalid upsert payload: ${msg}`);
|
|
6697
|
+
}
|
|
6698
|
+
return result.data;
|
|
6699
|
+
}
|
|
6395
6700
|
async function runUpsertGotchaSection(options) {
|
|
6396
|
-
const
|
|
6701
|
+
const rawJson = options.input === "-" ? await readStdin() : readFileSync(options.input, "utf-8");
|
|
6702
|
+
const payload = parseUpsertPayload(rawJson);
|
|
6703
|
+
if (payload.survey.designKey !== options.designKey) {
|
|
6704
|
+
throw new Error(
|
|
6705
|
+
`--design-key (${options.designKey}) does not match survey.designKey (${payload.survey.designKey})`
|
|
6706
|
+
);
|
|
6707
|
+
}
|
|
6708
|
+
const sectionMarkdown = renderGotchaSection({
|
|
6709
|
+
questions: payload.survey.questions,
|
|
6710
|
+
answers: payload.answers,
|
|
6711
|
+
designName: payload.designName,
|
|
6712
|
+
figmaUrl: payload.figmaUrl,
|
|
6713
|
+
designKey: payload.survey.designKey,
|
|
6714
|
+
designGrade: payload.survey.designGrade,
|
|
6715
|
+
analyzedAt: payload.analyzedAt,
|
|
6716
|
+
today: payload.today
|
|
6717
|
+
});
|
|
6397
6718
|
const currentContent = existsSync(options.file) ? readFileSync(options.file, "utf-8") : null;
|
|
6398
6719
|
const { state, newContent, plan } = renderUpsertedFile({
|
|
6399
6720
|
currentContent,
|
|
@@ -6406,7 +6727,8 @@ async function runUpsertGotchaSection(options) {
|
|
|
6406
6727
|
action: null,
|
|
6407
6728
|
sectionNumber: null,
|
|
6408
6729
|
wrote: false,
|
|
6409
|
-
userMessage: USER_MESSAGES[state] ?? null
|
|
6730
|
+
userMessage: USER_MESSAGES[state] ?? null,
|
|
6731
|
+
designKey: payload.survey.designKey
|
|
6410
6732
|
};
|
|
6411
6733
|
}
|
|
6412
6734
|
writeFileSync(options.file, newContent, "utf-8");
|
|
@@ -6415,7 +6737,8 @@ async function runUpsertGotchaSection(options) {
|
|
|
6415
6737
|
action: plan?.action ?? null,
|
|
6416
6738
|
sectionNumber: plan?.sectionNumber ?? null,
|
|
6417
6739
|
wrote: true,
|
|
6418
|
-
userMessage: null
|
|
6740
|
+
userMessage: null,
|
|
6741
|
+
designKey: payload.survey.designKey
|
|
6419
6742
|
};
|
|
6420
6743
|
}
|
|
6421
6744
|
function registerUpsertGotchaSection(cli2) {
|
|
@@ -6426,8 +6749,8 @@ function registerUpsertGotchaSection(cli2) {
|
|
|
6426
6749
|
"--design-key <key>",
|
|
6427
6750
|
"Canonical design key from gotcha-survey's response"
|
|
6428
6751
|
).option(
|
|
6429
|
-
"--
|
|
6430
|
-
"
|
|
6752
|
+
"--input <path>",
|
|
6753
|
+
"JSON payload path, or '--input=-' to read JSON from stdin (cac parses a bare '-' as a flag, so the '=' form is required)."
|
|
6431
6754
|
).action(async (rawOptions) => {
|
|
6432
6755
|
const parseResult = UpsertOptionsSchema.safeParse(rawOptions);
|
|
6433
6756
|
if (!parseResult.success) {
|
|
@@ -6894,14 +7217,16 @@ function formatNextSteps(opts) {
|
|
|
6894
7217
|
"",
|
|
6895
7218
|
" Next:",
|
|
6896
7219
|
" 1. Restart Cursor or reload MCP (so skills + MCP tools load in a fresh session)",
|
|
6897
|
-
" 2. In Agent chat, @ canicode-roundtrip with your Figma URL (or @ canicode-gotchas for survey-only)"
|
|
7220
|
+
" 2. In Agent chat, @ canicode-roundtrip with your Figma URL (or @ canicode-gotchas for survey-only)",
|
|
7221
|
+
" \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode (project `.cursor/mcp.json`), then reload MCP; otherwise skills keep using `npx canicode \u2026` (#433)."
|
|
6898
7222
|
].join("\n");
|
|
6899
7223
|
}
|
|
6900
7224
|
return [
|
|
6901
7225
|
"",
|
|
6902
7226
|
" Next:",
|
|
6903
7227
|
" 1. Restart Claude Code (the newly installed skills only load on a fresh session)",
|
|
6904
|
-
" 2. Run /canicode-roundtrip <figma-url>"
|
|
7228
|
+
" 2. Run /canicode-roundtrip <figma-url>",
|
|
7229
|
+
" \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so `analyze` / `gotcha-survey` tools load \u2014 otherwise skills shell out to `npx canicode \u2026` (#433)."
|
|
6905
7230
|
].join("\n");
|
|
6906
7231
|
}
|
|
6907
7232
|
if (cursor) {
|
|
@@ -6910,7 +7235,8 @@ function formatNextSteps(opts) {
|
|
|
6910
7235
|
" Next:",
|
|
6911
7236
|
" 1. Add Figma MCP to .cursor/mcp.json (see https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode and Figma MCP docs)",
|
|
6912
7237
|
" 2. Restart Cursor so Figma tools (e.g. use_figma) load",
|
|
6913
|
-
" 3. @ canicode-roundtrip with your Figma URL for full roundtrip"
|
|
7238
|
+
" 3. @ canicode-roundtrip with your Figma URL for full roundtrip",
|
|
7239
|
+
" \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per the Customization guide (`#cursor-mcp-canicode`), then reload MCP; otherwise skills keep using `npx canicode \u2026` (#433)."
|
|
6914
7240
|
].join("\n");
|
|
6915
7241
|
}
|
|
6916
7242
|
return [
|
|
@@ -6919,20 +7245,29 @@ function formatNextSteps(opts) {
|
|
|
6919
7245
|
" 1. Install Figma MCP:",
|
|
6920
7246
|
" claude mcp add -s project -t http figma https://mcp.figma.com/mcp",
|
|
6921
7247
|
" 2. Restart Claude Code (so the new skills + Figma MCP tools both load)",
|
|
6922
|
-
" 3. Run /canicode-roundtrip <figma-url>"
|
|
7248
|
+
" 3. Run /canicode-roundtrip <figma-url>",
|
|
7249
|
+
" \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so MCP tools load \u2014 otherwise skills shell out to `npx canicode \u2026` (#433)."
|
|
6923
7250
|
].join("\n");
|
|
6924
7251
|
}
|
|
6925
7252
|
var InitOptionsSchema = z.object({
|
|
6926
7253
|
token: z.string().optional(),
|
|
6927
7254
|
global: z.boolean().optional(),
|
|
6928
|
-
//
|
|
7255
|
+
// Declared positively as `--skills`; mri's built-in `--no-` prefix handling
|
|
7256
|
+
// still maps `--no-skills` to `skills: false`. Declaring the option
|
|
7257
|
+
// positively avoids cac's `(default: true)` artifact on negated flags.
|
|
6929
7258
|
skills: z.boolean().optional(),
|
|
6930
7259
|
/** Install `skills/cursor/*` into `.cursor/skills/` (canicode, gotchas, roundtrip — issue #407). */
|
|
6931
7260
|
cursorSkills: z.boolean().optional(),
|
|
6932
7261
|
force: z.boolean().optional()
|
|
6933
7262
|
});
|
|
6934
7263
|
function registerInit(cli2) {
|
|
6935
|
-
cli2.command(
|
|
7264
|
+
cli2.command(
|
|
7265
|
+
"init",
|
|
7266
|
+
"Set up canicode with Figma API token (never paste a token into agent chat \u2014 use FIGMA_TOKEN=\u2026 or the interactive prompt)"
|
|
7267
|
+
).option(
|
|
7268
|
+
"--token <token>",
|
|
7269
|
+
"Save Figma API token (use env/CLI only \u2014 not agent chat) and install Claude Code skills to .claude/skills/"
|
|
7270
|
+
).option("--global", "Install skills to ~/.claude/skills/ instead of ./.claude/skills/").option("--skills", "Install Claude Code skills into .claude/skills/ (default: on \u2014 pass --no-skills to opt out)").option("--cursor-skills", "Also install Cursor copies of canicode / canicode-gotchas / canicode-roundtrip under .cursor/skills/").option("--force", "Overwrite existing skill files without prompting (also for non-TTY/CI)").action(async (rawOptions) => {
|
|
6936
7271
|
try {
|
|
6937
7272
|
const parseResult = InitOptionsSchema.safeParse(rawOptions);
|
|
6938
7273
|
if (!parseResult.success) {
|
|
@@ -7044,6 +7379,10 @@ ${msg}`);
|
|
|
7044
7379
|
}
|
|
7045
7380
|
console.log(`CANICODE SETUP
|
|
7046
7381
|
`);
|
|
7382
|
+
console.log(
|
|
7383
|
+
` Never paste your token into Claude/Cursor chat \u2014 use FIGMA_TOKEN=\u2026 npx canicode init or this prompt only.
|
|
7384
|
+
`
|
|
7385
|
+
);
|
|
7047
7386
|
console.log(` canicode init --token YOUR_FIGMA_TOKEN`);
|
|
7048
7387
|
console.log(` Get token: figma.com > Settings > Personal access tokens
|
|
7049
7388
|
`);
|
|
@@ -7151,6 +7490,164 @@ function registerListRules(cli2) {
|
|
|
7151
7490
|
}
|
|
7152
7491
|
});
|
|
7153
7492
|
}
|
|
7493
|
+
var StepFourReportSchema = z.object({
|
|
7494
|
+
/** ✅ — Strategy A property write (or A's auto-fix branch) succeeded. */
|
|
7495
|
+
resolved: z.number().int().min(0),
|
|
7496
|
+
/** 📝 — Strategy C wrote a Figma annotation (or A/B fell back to one). */
|
|
7497
|
+
annotated: z.number().int().min(0),
|
|
7498
|
+
/**
|
|
7499
|
+
* 🌐 — `applyWithInstanceFallback` propagated the write up to the
|
|
7500
|
+
* source COMPONENT definition (only counted when
|
|
7501
|
+
* `allowDefinitionWrite: true`).
|
|
7502
|
+
*/
|
|
7503
|
+
definitionWritten: z.number().int().min(0),
|
|
7504
|
+
/**
|
|
7505
|
+
* ⏭️ — User said "skip", "n/a", or otherwise declined a per-question
|
|
7506
|
+
* confirmation (Strategy B opt-out, etc.).
|
|
7507
|
+
*/
|
|
7508
|
+
skipped: z.number().int().min(0)
|
|
7509
|
+
});
|
|
7510
|
+
var ReanalyzeForTallySchema = z.object({
|
|
7511
|
+
/**
|
|
7512
|
+
* Total remaining issues from the re-analyze. Maps to the existing
|
|
7513
|
+
* `issueCount` (analyze JSON) / `questions.length` (gotcha-survey JSON)
|
|
7514
|
+
* field — both downstream channels populate it.
|
|
7515
|
+
*/
|
|
7516
|
+
issueCount: z.number().int().min(0),
|
|
7517
|
+
/**
|
|
7518
|
+
* Issues the re-analyze flagged with `acknowledged: true` because they
|
|
7519
|
+
* matched a canicode-authored Figma annotation harvested in Step 5a.
|
|
7520
|
+
* From the analyze response's top-level `acknowledgedCount` (#371).
|
|
7521
|
+
*/
|
|
7522
|
+
acknowledgedCount: z.number().int().min(0)
|
|
7523
|
+
});
|
|
7524
|
+
z.object({
|
|
7525
|
+
/** ✅ resolved (passthrough from `stepFourReport.resolved`). */
|
|
7526
|
+
X: z.number().int().min(0),
|
|
7527
|
+
/** 📝 annotated. */
|
|
7528
|
+
Y: z.number().int().min(0),
|
|
7529
|
+
/** 🌐 definition writes propagated. */
|
|
7530
|
+
Z: z.number().int().min(0),
|
|
7531
|
+
/** ⏭️ skipped. */
|
|
7532
|
+
W: z.number().int().min(0),
|
|
7533
|
+
/** `X + Y + Z + W` — questions the SKILL acted on in Step 4. */
|
|
7534
|
+
N: z.number().int().min(0),
|
|
7535
|
+
/** `reanalyzeResponse.issueCount` — total remaining after re-analyze. */
|
|
7536
|
+
V: z.number().int().min(0),
|
|
7537
|
+
/**
|
|
7538
|
+
* `reanalyzeResponse.acknowledgedCount` — the slice of `V` that carries
|
|
7539
|
+
* a canicode annotation (counted at half weight by the density score
|
|
7540
|
+
* per #371, but still surfaced as remaining).
|
|
7541
|
+
*/
|
|
7542
|
+
V_ack: z.number().int().min(0),
|
|
7543
|
+
/** `V - V_ack` — issues with no annotation; the user's follow-up backlog. */
|
|
7544
|
+
V_open: z.number().int().min(0)
|
|
7545
|
+
});
|
|
7546
|
+
|
|
7547
|
+
// src/core/roundtrip/compute-roundtrip-tally.ts
|
|
7548
|
+
function computeRoundtripTally(args) {
|
|
7549
|
+
const { stepFourReport, reanalyzeResponse } = args;
|
|
7550
|
+
const { resolved, annotated, definitionWritten, skipped } = stepFourReport;
|
|
7551
|
+
const { issueCount, acknowledgedCount } = reanalyzeResponse;
|
|
7552
|
+
if (acknowledgedCount > issueCount) {
|
|
7553
|
+
throw new Error(
|
|
7554
|
+
`computeRoundtripTally: reanalyzeResponse.acknowledgedCount (${acknowledgedCount}) cannot exceed issueCount (${issueCount}). Acknowledged issues are a subset of remaining issues.`
|
|
7555
|
+
);
|
|
7556
|
+
}
|
|
7557
|
+
return {
|
|
7558
|
+
X: resolved,
|
|
7559
|
+
Y: annotated,
|
|
7560
|
+
Z: definitionWritten,
|
|
7561
|
+
W: skipped,
|
|
7562
|
+
N: resolved + annotated + definitionWritten + skipped,
|
|
7563
|
+
V: issueCount,
|
|
7564
|
+
V_ack: acknowledgedCount,
|
|
7565
|
+
V_open: issueCount - acknowledgedCount
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7569
|
+
// src/cli/commands/roundtrip-tally.ts
|
|
7570
|
+
var RoundtripTallyCliOptionsSchema = z.object({
|
|
7571
|
+
analyze: z.string().min(1),
|
|
7572
|
+
step4: z.string().min(1)
|
|
7573
|
+
});
|
|
7574
|
+
function parseJsonFile(label, path, raw) {
|
|
7575
|
+
try {
|
|
7576
|
+
return JSON.parse(raw);
|
|
7577
|
+
} catch (err) {
|
|
7578
|
+
throw new Error(
|
|
7579
|
+
`roundtrip-tally: ${label} is not valid JSON (${path})`,
|
|
7580
|
+
{ cause: err }
|
|
7581
|
+
);
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
async function readUtf8File(label, path) {
|
|
7585
|
+
try {
|
|
7586
|
+
return await readFile(path, "utf-8");
|
|
7587
|
+
} catch (err) {
|
|
7588
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
7589
|
+
throw new Error(`roundtrip-tally: cannot read ${label} (${path}): ${detail}`, {
|
|
7590
|
+
cause: err
|
|
7591
|
+
});
|
|
7592
|
+
}
|
|
7593
|
+
}
|
|
7594
|
+
async function computeRoundtripTallyFromSavedFiles(args) {
|
|
7595
|
+
const [analyzeRaw, step4Raw] = await Promise.all([
|
|
7596
|
+
readUtf8File("--analyze", args.analyzePath),
|
|
7597
|
+
readUtf8File("--step4", args.step4Path)
|
|
7598
|
+
]);
|
|
7599
|
+
const analyzeParsed = parseJsonFile("--analyze", args.analyzePath, analyzeRaw);
|
|
7600
|
+
const step4Parsed = parseJsonFile("--step4", args.step4Path, step4Raw);
|
|
7601
|
+
const reanalyzeResult = ReanalyzeForTallySchema.safeParse(analyzeParsed);
|
|
7602
|
+
if (!reanalyzeResult.success) {
|
|
7603
|
+
const msg = reanalyzeResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
7604
|
+
throw new Error(
|
|
7605
|
+
`roundtrip-tally: --analyze must include issueCount and acknowledgedCount (${args.analyzePath}): ${msg}`
|
|
7606
|
+
);
|
|
7607
|
+
}
|
|
7608
|
+
const stepFourResult = StepFourReportSchema.safeParse(step4Parsed);
|
|
7609
|
+
if (!stepFourResult.success) {
|
|
7610
|
+
const msg = stepFourResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
7611
|
+
throw new Error(
|
|
7612
|
+
`roundtrip-tally: --step4 must match StepFourReport (${args.step4Path}): ${msg}`
|
|
7613
|
+
);
|
|
7614
|
+
}
|
|
7615
|
+
return computeRoundtripTally({
|
|
7616
|
+
stepFourReport: stepFourResult.data,
|
|
7617
|
+
reanalyzeResponse: reanalyzeResult.data
|
|
7618
|
+
});
|
|
7619
|
+
}
|
|
7620
|
+
function registerRoundtripTally(cli2) {
|
|
7621
|
+
cli2.command(
|
|
7622
|
+
"roundtrip-tally",
|
|
7623
|
+
"Print the Step 5 roundtrip tally from re-analyze JSON and Step 4 outcome counts"
|
|
7624
|
+
).option("--analyze <path>", "Path to re-analyze JSON (`canicode analyze --json` output)").option("--step4 <path>", "Path to Step 4 structured counts (resolved / annotated / definitionWritten / skipped)").example(" canicode roundtrip-tally --analyze ./reanalyze.json --step4 ./step4-report.json").action(async (rawOptions) => {
|
|
7625
|
+
const parsed = RoundtripTallyCliOptionsSchema.safeParse(rawOptions);
|
|
7626
|
+
if (!parsed.success) {
|
|
7627
|
+
const msg = parsed.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
7628
|
+
console.error(`
|
|
7629
|
+
roundtrip-tally requires --analyze and --step4:
|
|
7630
|
+
${msg}`);
|
|
7631
|
+
process.exit(1);
|
|
7632
|
+
}
|
|
7633
|
+
try {
|
|
7634
|
+
const tally = await computeRoundtripTallyFromSavedFiles({
|
|
7635
|
+
analyzePath: parsed.data.analyze,
|
|
7636
|
+
step4Path: parsed.data.step4
|
|
7637
|
+
});
|
|
7638
|
+
console.log(JSON.stringify(tally, null, 2));
|
|
7639
|
+
trackEvent(EVENTS.ROUNDTRIP_TALLY);
|
|
7640
|
+
} catch (err) {
|
|
7641
|
+
if (err instanceof Error) {
|
|
7642
|
+
trackError(err, { command: "roundtrip-tally" });
|
|
7643
|
+
console.error(err.message);
|
|
7644
|
+
} else {
|
|
7645
|
+
console.error(err);
|
|
7646
|
+
}
|
|
7647
|
+
process.exit(1);
|
|
7648
|
+
}
|
|
7649
|
+
});
|
|
7650
|
+
}
|
|
7154
7651
|
|
|
7155
7652
|
// src/cli/internal-commands.ts
|
|
7156
7653
|
var INTERNAL_COMMANDS = [
|
|
@@ -8429,9 +8926,9 @@ function registerCalibrateEvaluate(cli2) {
|
|
|
8429
8926
|
if (!existsSync(conversionPath)) {
|
|
8430
8927
|
throw new Error(`Conversion file not found: ${conversionPath}`);
|
|
8431
8928
|
}
|
|
8432
|
-
const { readFile:
|
|
8433
|
-
const analysisData = JSON.parse(await
|
|
8434
|
-
const conversionData = JSON.parse(await
|
|
8929
|
+
const { readFile: readFile5 } = await import('fs/promises');
|
|
8930
|
+
const analysisData = JSON.parse(await readFile5(analysisPath, "utf-8"));
|
|
8931
|
+
const conversionData = JSON.parse(await readFile5(conversionPath, "utf-8"));
|
|
8435
8932
|
let fixtureName;
|
|
8436
8933
|
if (options.runDir) {
|
|
8437
8934
|
const dirName = resolve(options.runDir).split(/[/\\]/).pop() ?? "";
|
|
@@ -10289,6 +10786,7 @@ registerVisualCompare(cli);
|
|
|
10289
10786
|
registerInit(cli);
|
|
10290
10787
|
registerConfig(cli);
|
|
10291
10788
|
registerListRules(cli);
|
|
10789
|
+
registerRoundtripTally(cli);
|
|
10292
10790
|
registerCalibrateAnalyze(cli);
|
|
10293
10791
|
registerCalibrateEvaluate(cli);
|
|
10294
10792
|
registerCalibrateGapReport(cli);
|
|
@@ -10312,6 +10810,7 @@ cli.help((sections) => {
|
|
|
10312
10810
|
section.body = section.body.split("\n").filter((line) => !INTERNAL_COMMANDS.some((cmd) => line.includes(cmd))).join("\n");
|
|
10313
10811
|
}
|
|
10314
10812
|
}
|
|
10813
|
+
sections.splice(1, 0, { body: ` ${pkg2.description}` });
|
|
10315
10814
|
sections.push(
|
|
10316
10815
|
{
|
|
10317
10816
|
title: "\nSetup",
|
|
@@ -10334,7 +10833,8 @@ cli.help((sections) => {
|
|
|
10334
10833
|
` $ canicode analyze "https://www.figma.com/design/..." --api`,
|
|
10335
10834
|
` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
|
|
10336
10835
|
` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`,
|
|
10337
|
-
` $ canicode gotcha-survey "https://www.figma.com/design/..." --json
|
|
10836
|
+
` $ canicode gotcha-survey "https://www.figma.com/design/..." --json`,
|
|
10837
|
+
` $ canicode roundtrip-tally --analyze ./reanalyze.json --step4 ./step4.json`
|
|
10338
10838
|
].join("\n")
|
|
10339
10839
|
},
|
|
10340
10840
|
{
|